Telefon rehberi deyince eskiden babamın kullandığı o küçük defter gelir bazen aklıma. O zamanlar sadece ev telefonu vardı, üstelik her evde de yoktu ve yaptığı tek şey arama idi. Şimdiki telefonların ise yapmadığı bir şey yok. O yıllarda birileri çıkıp bu günleri anlatsaydı diyeceğimiz tek şey bu adam deli olurdu.

Tabiki hiçkimse bu günleri hayal ederek icat yapmadı. Yani kimse bugünlerin böyle olacağını hayal edemezdi. Herşey adım adım ilerledi ve iş olacağına vardı. Bu hep böyledir. Bugün kuramadığımız hayaller yarın bizi bekliyor. Bugün imkansız gibi görünen, hatta hiç görünmeyen şeylere yarın yakın olabiliriz.
Android
Android sistemi, bilgileri çoğunlukla veri tabanlarında tutar. Fakat bilgilerin tutulduğu bu veri tabanlarına erişim tamamen soyutlanmıştır. Mesela sistemdeki veri tabanlarına ulaşmak için veri tabanı dosyalarının adresleri verilmez. Bunun yerine, veri tabanını kod tarafında gerçeklemiş sınıflara temsili bir adres verilir.
ContactsContract.Contacts.CONTENT_URI
Bu adres telefon rehberinin adresi. Bu tür adresler content:// ile başlayan, temsili bir adres teşkil eden Uri nesneleridir. Bu nesneler genel anlamda bir yol bilgisi tutar. Ancak burada bu yol kesinlikle veri tabanı dosyasının direk yolu değildir. Temsilidir. Biz bu yolu kullanarak, direk değil, bu soyutlama üzerinden erişim sağlarız ve veri tabanı sorgusunu bu adres üzerinden yaparız. Soyutlama neticesinde kazanılan fayda ise tabiki kolaylık ve hizmet. Mesela direk veri tabanı sorgusunu herkes yazamayabilir. Yani herhes veri tabanı kursuna gitmemiş olabilir, bilmiyor veya anlamıyor olabilir. Bu sebeple programcıya hizmet verecek nesneler ve metotları gerekir. Yani bir programcı veri tabanı konusunda derin bilgilere sahip olmadan da basit sorgular yapabilmeli. Android sisteminde bu yardımcı nesne
Cursor
nesnesidir.
ContentResolver resolver = context.getContentResolver();
Cursor cursor = resolver.query(ContactsContract.Contacts.CONTENT_URI,
null,
null,
null,
null);
Bu şekilde telefon rehberinin tüm kayıtlarına erişiriz. Veri tabanıyla ilgili bilmemiz gereken en önemli bilgi, tablo görünümünde olduklarıdır. Yani satır ve sütunlardan oluşur.
_id | name | photo |
---|---|---|
1 | ali | content://contacts/1/photo |
5 | veli | null |
7 | deli | null |
_id, name, photo bu tablonun sütunlarıdır. Sütunlar sabittir, satırlar ise kayıtlı bilgilerdir ve değişebilir. Yani veri tabanında hiç satır olmayabilir ama sütun hep vardır. Ve bu sütunları Cursor
nesnesi üzerinden öğrenebiliriz.
cursor.getColumnNames()
Bu metot bize bir String[]
dizi döndürür.
final Cursor cursor = getContentResolver().query(
ContactsContract.Contacts.CONTENT_URI,
null,
null,
null,
null
);
if(cursor == null) return;
for (String column : cursor.getColumnNames()) {
Log.i("column", column);
}
I/column: sort_key
photo_uri
send_to_voicemail
contact_status
contact_status_label
pinned
display_name
phonebook_label_alt
phonebook_bucket
contact_status_res_package
in_default_directory
photo_id
custom_ringtone
_id
times_contacted
phonebook_label
display_name_alt
lookup
phonetic_name
last_time_contacted
contact_last_updated_timestamp
has_phone_number
in_visible_group
display_name_source
photo_file_id
is_user_profile
contact_status_ts
sort_key_alt
phonebook_bucket_alt
contact_presence
starred
photo_thumb_uri
contact_status_icon
contact_chat_capability
phonetic_name_style
name_raw_contact_id
Ancak bir rehber kaydı bundan daha fazla bilgiye sahiptir. Mesela yukarıdaki sütunların içinde numara bilgisini taşıyan bir sütun yok. Sadece bu rehber kaydının bir numaraya sahip olup olmadığı bilgisini tutan has_phone_number
adında bir sütun bulunuyor. Bu sütunun altına denk gelen satırların bu alanındaki değer 1
ise numarası var, 0
ise numarası yok demektir. Bu da demek oluyorki, rehbere bir kişi kaydederken numara girmeyebiliriz. Yani sadece isim girerek kayıt yapmamız mümkün.
Biz rehber kayıtlarına erişirken şu sütunları kullanacağız :
_id
display_name
photo_thumb_uri
sort_key
_id
sütunu veri tabanlarında benzersiz bir değere sahiptir. Yani her bir kaydın kendine özel bir id değeri vardır.
display_name
kişinin ismi. Bu alan diğer alanlardan herhangi biriyle aynı olabilir. Yani bu alan değişkendir ve benzersiz değildir. Benzersizlik üzerine yapılacak başvurular _id
sütunu üzerinden olacaktır. Benzerlik üzerine yapılacak başvurular ise bu sütun veya diğerleri üzerinden yapılabilir. Mesela ismi ahmet olan kişiler.
photo_thumb_uri
kayda ait resmin küçültülmüş bir versiyonuna Uri
'dir. Resim yoksa null
'dır.
sort_key
sütunu ise sıralama yapmak için. Ama bu sıralama sorgu anında yapılan bir sıralama. Yani gelen sonuçlar alfabetik sırada olacak.
Sorgu Özelleştirme
final Cursor cursor = getContentResolver().query(
ContactsContract.Contacts.CONTENT_URI,
null,
null,
null,
null);
Bu şekilde bir sorgu, tüm rehber kayıtlarını, tüm sütunları ile elde etmemizi sağlar. Elde ettiğimiz şey veri tabanındaki satırlar. Demedik mi veri tabanı bir tablo gibidir. Biz az önce telefon rehberi tablosunu elde ettik. Cursor imleç anlamına geliyor. Hani bilgisayarda veya telefonda birşey yazarken yanıp sönen bir | çizgi varya, işte o çizginin adı imleç. Bu çizgi bize yazının neresinde olduğumuzu gösterir, tuşladığımız harfler tam bu çizginin bulunduğu yerde görünür ve biz tuşlara basmaya devam ettikçe ilerler. Ayrıca dilersek imleci yazının herhangi bir yerine cart diye zıplatabiliriz. Veri tabanı için kullandığımız Cursor
nesnesinin yaptığı iş de tam olarak budur işte. Veri tabanı içinde ileri-geri gitmek ve bulunduğumuz satırdan okuma yapmak için kullanırız bu nesneyi. Bu nesne için mevzu satırdır. Bilgisayar veya telefonlardaki imleç harf harf işler, veri tabanındaki imleç ise satır satır işler. Bilgisayar veya telefonlardaki imleçi nasılki istediğimiz harf konumuna zıplatabiliyorsak, aynı şekilde veri tabanında da imleçi istediğimiz satıra zıplatabiliriz. Mesela yukarıdaki sorgu ifadesinden sonra imleçi elde ettiğimiz tablonun son satırına zıplatmak için şöyle yapabiliriz.
cursor.moveToPosition(cursor.getCount() - 1);
Bir tablonun programlamada karşılığı tabiki iki boyutlu bir dizidir. Yukarıdaki ifade x
boyutunu belirler. Yani yukarıdan aşağı kaçıncı satır olduğunu. y
boyutunu ise sütunlar belirler, yani soldan sağa kaçıncı sütun olduğu. Ben dizileri apartmanlara benzetiyorum. Çok boyutlu dizileri ise apartmanlardan oluşan sitelere. 10 tane 5 katlı apartmandan oluşan bir sitede bir daireyi tarif edebilmek için neler gerekli? Bir site en başta A Blok, B Blok, C Blok diye bloklara ayrılır. Bunlar sütunlardır. Herhangi bir bloğa girdiğimizde ise kat numaraları vardır, 1. Kat, 2. Kat, 3. Kat. İşte bunlar satırlardır. Bu şekilde, A Blok 3. Kat dediğimizde nereyi tarif ettiğimiz tam olarak belirlenmiş olur. Biz yukarıdaki kod satırıyla kat numarasını belirlemiş oluyoruz. Yani zemin kat. Yani en alt kat. Eğer olmayan bir kat numarası girersek, yani mesela 5 katlı bir apartman için 7. Kat diye bir şey söylersek hata etmiş oluruz. Bu hata dizilerde olduğu gibi CursorIndexOutOfBoundsException
hatası, yani olmayan bir kat talep etme hatası alırız. Diziler için bu hata neydi? ArrayIndexOutOfBoundsException
. Yani veri tabanından bir satır talep ettiğimizde sınırları aşmamaya dikkat etmeliyiz. Bildiğimiz normal bir dizinin son elemanını nasıl alıyorduk?
lastItem = array[array.length - 1]
Biz de aynen bunu yaptık işte.
cursor.moveToPosition(cursor.getCount() - 1);
Fakat gördüğün gibi bu kod satırında bir atama ifadesi yok. Yani son satıra gelmesine geldik ama eee? Nerde bu satır? Bir atama ifadesinin olmamasının sebebi, bizim imleci hareket ettirmekten başka bir şey yapmamış olmamız. Yani sadece yeri belirledik. Yani mesela A Blok 3. Kat'a çıktık ama daireye girmedik. Bu noktada şöyle bir şey söyleyebiliriz, Cursor
nesnesi dizinin kendisi değil. Sinema salonlarında koltuk gösteren görevli gibi. Peki ya salonda yer yoksa? Yani veri tabanında hiçbir bilgi kayıtlı değilse? Bu durumda yukarıdaki ifade bize hata olarak geri döner. Çünkü veri yoksa cursor.getCount()
metodu 0
döndürür. Ve cursor.getCount() - 1
işleminin sonucu -1
olur ve bu da geçerli bir yer belirtmediğinden dolayı hata ile sonuçlanır.
if (cursor.getCount() > 0) {
cursor.moveToPosition(cursor.getCount() - 1);
}
Doğru yaklaşım budur. Ama daha da doğrusu var. Cursor
nesnesinin yardımcı bir nesne olduğunu söylemiştik sanırım. Son satıra gitmek için bu nesnenin cursor.moveToLast()
metodunu kullanmamız daha doğru bir yaklaşım. Ve bu metot boolean bir değer döndürür. Bu döndürdüğü değerin anlamı ise true için son eleman var, false için son eleman yok, yani hiç eleman yok anlamına gelir. Eğer en az 1
eleman var ise true döndürür ve aynı zamanda da o son elemana zıplar.
if (cursor.moveToLast()) {
//son satırda
}
if
deyiminin içindeki kodlar son satır üzerinde işlem görür. Eğer veri tabanında hiç kayıt yoksa bu if
deyimi yürütülmez tabiki. Bu metodun sadece ve sadece cursor.getCount()
metodunun sıfır döndürdüğünde false olacağı aşikar. En az bir kayıt olduğu sürece de true döndüreceği kabak gibi ortada.
Cursor
nesnesi daha başka metotlar da sunar.
if (cursor.moveToFirst()) {
//ilk satır
}
Bu metot da boolean değer döndürür ve önceki söylediklerimiz bu metot için de aynen geçerli.
cursor.moveToPosition(index)
metodu ile istediğimiz satıra zıplayabiliyorduk değil mi? Bu metot, üzerinde çalıştığımız veri tabanının tamamını göz önünde bulundurarak bütüne dayalı işlem yapar. Bütün satırlar arasından şu index'teki satır demiş oluyoruz. Bunun yanısıra bir de cursor.move(index)
metodu var. Bu metodun farkı, cursor
'un o an bulunduğu satırı dikkate alması. Mesela cursor
32. satırda ise, cursor.move(1)
ifadesi ile 33. satıra zıplamış olur. İndex olarak negatif değerler de verilebilir. Negatif değer geriye doğru, pozitif değer ileri doğru hareket eder. Ve bu hareket, o an bulunduğu satırın index değerine göre olur. Mesela index olarak -1
dersek bir önceki satıra zıplamış oluruz. Tabi eğer böyle bir satır var ise. Var ise true, yok ise false döndürür. Eğer kulağımızı tersten tutmak istersek şöyle bir saçmalık yazabiliriz.
if (cursor.moveToLast()) {
cursor.move(-(cursor.getCount() - 1));
}
İlk önce son satıra gidiyoruz ve ordan da satır sayısı kadar geriye gidiyoruz, yani ilk satıra.
Cursor
nesnesi hakkında bilmemiz gereken bir husus da şu ki, sorgu ifadesinden sonra imlecin henüz hiçbir yeri göstermediğidir.
final Cursor cursor = getContentResolver().query(
ContactsContract.Contacts.CONTENT_URI,
null,
null,
null,
null
);
Şu an imleç hiçbir yerde değil. Yani hiçbir yeri göstermiyor. Birazdan satır okumayı göreceğiz, bu ifadeden hemen sonra okuma yapmaya çalışırsak hata alırız. Öncelikle imleci bir yere konumlandırmamız gerek okumaya başlamak için. Genel olarak veri tabanını baştan sona okumak isteriz. Yani ilk satırdan başlayıp son satıra kadar. Bu durumda ilk yapacağımız işlem imleci ilk satıra konumlandırmaktır. Ve bunu moveToFirst()
ile yapabileceğimizi az önce gördük. Eğer bu metot true döndürürse demekki veri tabanında en az bir kayıt varmış deriz. Ve metodun işlevi neticesinde imleç ilk satıra konumlanmış olur.
if (cursor.moveToFirst()) {
//imleç ilk satıra konumlandı
}
Bu konumu değiştirmediğimiz sürece hep aynı satır üzerinde yapılır okuma. Teorimizin ilk kısmını gerçekleştirdik, yani veri tabanını baştan sona okumak için gerekli olan ilk adımı, yani ilk satıra imleci konumlandırmayı gerçekleştirdik. Şimdi işimiz satır satır ilermek. Bu iş için Cursor
nesnesi moveToNext()
metodunu gururla sunar.
if (cursor.moveToFirst()) {
//imleç ilk satıra konumlandı
if(cursor.moveToNext()){
//sonraki satır
if(cursor.moveToNext()){
//sonraki satır
}
}
}
moveToNext()
metodunu her işlettiğimizde bi sonraki satıra geçeriz. Ancak elbette bunu yukarıdaki gibi değil, bir döngü ile kullanırız.
if (cursor.moveToFirst()) {
//imleç ilk satıra konumlandı
while (cursor.moveToNext()){
//sonraki satır
}
}
İşte bu döngü son satıra kadar tüm satırları bir bir ilerleyerek gezer. Bu döngünün aslında bir do...while
döngüsü olması gerektiği ilk bakışta anlaşılıyor
if (cursor.moveToFirst()) {
do {
//işlemler
}while (cursor.moveToNext());
}
Cursor
nesnesi ile yaptığımız ilk işlem her neresi olursa olsun imleci konumlandırmak. Yani bunun için illa moveToFirst()
metodunu kullanmak zorunda değiliz. moveToLast()
metodunu da kullanabiliriz, moveToNext()
metodunu da kullanabiliriz, moveToPosition(index)
metodunu da kullanabiliriz. Yeterki bir yere konumlansın. Konumlandırmadan önce okuma yapamayız. Bu arada moveToNext()
metodu da ilk konumlandırmayı yapabiliyor. Yani yukarıdaki döngüyü daha sade yapabiliriz.
while (cursor.moveToNext()) {
//işlemler
}
moveToNext()
metodunu ilk çağırdığımızda imleç ilk satıra konumlanır. Ve sonraki çağrılarda satır satır ilerler. Okuma yaparken biz bu döngüyü kullanacağız.
Cursor
nesnesinin daha bir çok metodu var ve yapacağımız işi farklı farklı yöntemlerle yapabiliriz. Ancak bu gördüğümüz metotlar işimizi görmeye yeter. Yani veri tabanında gezinmek için. Okuma yapmak için ise get
ile başlayan ve tüm veri türlerini okuyabileceğimiz kardeş metotlar var. Bu metotlar imlecin konumlanmış olduğu satırdaki veriyi okur.
getInt(index)
getString(index)
getLong(index)
getFloat(index)
getDouble(index)
getBlob(index)
getShort(index)
Şu an aklımızda iki soru olmalı. Birincisi bu metotların aldığı index neyin index'i? İkinci soru ise, alacağımız verinin türünü nasıl bileceğiz?
Birinci soruyla başlayalım. Bu metotların aldıkları index sütun index'i. Yukarıda bir yerlerde demiştik ya A Blok, B Blok, C Blok falan diye. Yani tablonun tepesindeki sütunlar. Bu veri tabanı dediğimiz şey madem iki boyutlu bir dizi, o halde veriye ulaşmak için sütunun index'ini de vermemiz gerekmez mi? Yani iki boyutlu bir dizi iki boyutlu bir düzlem demektir ve bunu biz x, y
düzlemi olarak görürüz. Yani bu düzlemde bir konum belirtmek için iki nokta gerekir. İlk konum zaten hazır. Yani veri tabanında baştan başlayıp tek tek ilerleyeceğiz. Buna eğer x
düzlemi dersek, bize şimdi y
düzlemi gerekli. Gayet mantıklı. Çünkü dairenin kaçıncı katta olduğunu belirledik. Aslında belirleme ihtiyacı duymuyoruz çünkü baştan sona ilerleyeceğiz. Ancak burada gerçek hayattakinden küçük bir farkla. O da şu ki, gerçek hayatta apartmana zemin kattan gireriz ve kat kat çıkarız. Ancak veri tabanında tam tersine en üst kattan başlıyoruz. Gerçi bu biraz da bakış açısına bağlı. Yani diyelimki moveToFirst()
metoduyla ilk satıra geldik, bu ilk satırı zemin kat olarak da düşünebilirsin en üst kat olarak da düşünebilirsin. Ama veri tabanının satırlar yığını olduğunu göz önüne alırsak, ilk satır yığının en üstündeki satırdır.
Bu bahsettiğimiz iki boyutlu dizi, bildiğimiz normal iki boyutlu java dizisi. İki boyutlu bir dizide bir veriye ulaşmak için ise tabiki iki tane sayısal index belirtiriz.
int[][] dizi = {
{1,2,3},
{4,5,6},
{7,8,9}
};
int test = dizi[1][2];//test = 6
İlk index değeri otomatik olarak verileceği için bizim ikinci index'i belirtmemiz gerek. Yani kaçıncı sütun olduğunu sayısal bir değer ile girmemiz gerek. Peki biz bu sütunların sayısal index değerini nasıl bulacağız? Çünkü sütunlar String
olarak isimlendirilmiş. Bize sayısal bir değer lazım. Mesela _id sütunu kaçıncı index oluyor?
Bu sütunların dizide hangi index'e denk geldiğini öğrenmek için getColumnIndex(String sütunIsmi)
metodunu kullanacağız.
int idColumn = cursor.getColumnIndex("_id");
Tüm sütun index'lerini bu şekilde bulacağız.
int nameColumn = cursor.getColumnIndex("display_name");
int sortKeyColumn = cursor.getColumnIndex("sort_key");
int thumbNailPhotoColumn = cursor.getColumnIndex("photo_thumb_uri");
İndex'leri aldığımıza göre artık dizideki veriyi okuyabiliriz. Ancak biz bu veriyi hangi türde okuyacağız, bu index'teki verinin türü nedir? İşte bu da ikinci sorumuzdu ve şimdi bunu bulacağız.
final Cursor cursor = getContentResolver().query(
ContactsContract.Contacts.CONTENT_URI,
null,
null,
null,
null
);
if (cursor == null) return;
if (!cursor.moveToNext()) return;
int idColumn = cursor.getColumnIndex("_id");
int type = cursor.getType(idColumn);
switch (type) {
case Cursor.FIELD_TYPE_STRING:
log.w("string");
break;
case Cursor.FIELD_TYPE_INTEGER:
log.w("int veya long");
break;
case Cursor.FIELD_TYPE_FLOAT:
log.w("float");
break;
case Cursor.FIELD_TYPE_BLOB:
log.w("blob");
break;
case Cursor.FIELD_TYPE_NULL:
log.w("null");
break;
}
Burada yine herhangi bir işlem yapmadan önce imlecin konumlanması gerek.
if (!cursor.moveToNext()) return;
İlk satıra konumlandı. Sonrasında hangi sütunun hangi tür veri olduğunu getType
metodu ile öğreniyoruz. Ancak sana sevindirici bir haberim var, bu veri türlerini böyle sınayıp da almak zorunda değiliz. Mesela yukarıda aldığımız _id
sütunu int veya long çıktısı veriyor ama biz String
olarak da alabiliriz. Daha doğrusu her türü String
olarak alabiliriz, blob
hariç.
String id = cursor.getString(idColumn);
Ama zaten biz neyle uğraştığımızı üç aşşağı beş yukarı biliriz, ona göre veriyi alırız.
Oku
Ancak tüm sütunları okumak her zaman isteyeceğimiz bir şey değil. Biz sadece yukarıda belirttiğimiz 4 sütunu almak istiyoruz. Yani kendi isteğimize göre biz bu sütunlardaki bilgileri almak istediğimize karar verdik. Yani herkes dilediği sütunu alabilir.
Ve bu sütunları bir dizi içine koyarız.
private static final String[] CONTACT_COLUMNS = {
"_id",
"display_name",
"photo_thumb_uri",
"sort_key"
};
Hepsi bu kadar. Sorgu yapabiliriz.
final Cursor cursor = getContentResolver().query(
ContactsContract.Contacts.CONTENT_URI,
CONTACT_COLUMNS,
null,
null,
null
);
Bu şekilde, sadece belirttiğimiz sütunlara ait bilgileri alırız. query
metodunun parametrelerini AndroidStudio'da görebiliyoruz.

Biz az önce projection
parametresini belirttik. Tahmin edebildiğin gibi, diğer parametreleri de ihtiyacımıza göre düzenleyebiliriz. Mesela sortOrder
parametresi, gelecek olan sonuçların sıralama kriterini belirtir. Eğer bu değer yukarıdaki gibi null
geçilirse herhangi bir sıralama yapılmaz ve veriler veri tabanının yapısına göre gelir.
Yukarıda sonuçları alfabetik sırada alacağımızı söylemiştik. Bu sıralama kişinin ismine göre olacak. Ben rehbere sallama bir kaç kişi kaydettim.

Bu rehbere göre yukarıdaki sorguyu alfabetik sırada olacak şekilde şöyle yazarız.
final Cursor cursor = getContentResolver().query(
ContactsContract.Contacts.CONTENT_URI,
CONTACT_COLUMNS,
null,
null,
"display_name" + " asc"
);
asc
, ascend anlamına kullanılıyor. Yani artan sırada, yani küçükten büyüğe. Bu bir veri tabanı sorgu kelimesidir. Daha doğrusu, sorgudan elde etiğimiz verilerin sıralamasını özelleştirmek için kullanılan bir kelimedir ve veri tabanı sistemine özeldir. Bu arada android SQLite kullanır. Ve genellikle vari tabanı sorgu kelimeleri büyük harfle yazılır, ASC. Ancak veri tabanı sorguları büyük-küçük harf ayırımı yapmaz. Şimdi bu sorguyu işletelim.
final Cursor cursor = getContentResolver().query(
ContactsContract.Contacts.CONTENT_URI,
CONTACT_COLUMNS,
null,
null,
"display_name" + " asc"
);
if (cursor == null) return;
while (cursor.moveToNext()) {
final String name = cursor.getString(cursor.getColumnIndex("display_name"));
Log.i("name", name);
}
Sonuç :
I/name: 123456789
987654321
ali
ayşe
deli
fatma
hayriye
veli
çiçek
Sıralama tamam ama ismi olmayan kayıtlar başta geldi. İsmi olmayan kayıtların numaralarının otomatik olarak isim olduğunu söylemiştik değil mi? İşte burada bunu da görüyoruz. Ama bunların listenin sonunda olması daha doğru olurdu. Ayrıca türkçe karakterle başlayan isim de yanlış sırada geldi. Bu sorunları düzeltmek için sort_key
sütununu kullanacağız. Bu sütun sıralamayı sorunsuz şekilde yapıyor.
final Cursor cursor = getContentResolver().query(
ContactsContract.Contacts.CONTENT_URI,
CONTACT_COLUMNS,
null,
null,
"sort_key" + " asc"
);
if (cursor == null) return;
while (cursor.moveToNext()) {
final String name = cursor.getString(cursor.getColumnIndex("display_name"));
Log.i("name", name);
}
Sonuç :
I/name: ali
ayşe
çiçek
deli
fatma
hayriye
veli
123456789
987654321
Rehber kaydının ismini almak için,
final String name = cursor.getString(cursor.getColumnIndex("display_name"));
satırını kullandık. cursor.getString
metodu (ve kardeşleri) parametre olarak sütun indeksini alıyor demiştik. Yani alacağımız bilginin kaçıncı sütuna ait bilgi olduğunu belirtiyoruz. Bu index, query
metodunda belirttiğimiz dizideki sütunlardan biri olmak zorunda. Yoksa hata alırız.
final String[] CONTACT_COLUMNS = {
"_id",
"display_name",
"photo_thumb_uri",
"sort_key"
};
final Cursor cursor = getContentResolver().query(
ContactsContract.Contacts.CONTENT_URI,
CONTACT_COLUMNS,
null,
null,
"sort_key" + " asc"
);
if (cursor == null) return;
while (cursor.moveToNext()) {
final String name = cursor.getString(cursor.getColumnIndex("phonetic_name"));
Log.i("name", name);
}
java.lang.IllegalStateException: Couldn't read row 0, col -1 from CursorWindow. Make sure the Cursor is initialized correctly before accessing data from it
phonetic_name
sütunu bu veri tabanında olduğu halde hata verdi çünkü biz sorguyu yaparken bu sütunu belirtmedik.
Şİmdi diğer sütunlardaki bilgileri de alalım.
final String[] CONTACT_COLUMNS = {
"_id",
"display_name",
"photo_thumb_uri",
"sort_key"
};
final Cursor cursor = getContentResolver().query(
ContactsContract.Contacts.CONTENT_URI,
CONTACT_COLUMNS,
null,
null,
"sort_key" + " asc"
);
if (cursor == null) return;
while (cursor.moveToNext()) {
final String id = cursor.getString(cursor.getColumnIndex(CONTACT_COLUMNS[0]));
final String name = cursor.getString(cursor.getColumnIndex(CONTACT_COLUMNS[1]));
final String photoThumb = cursor.getString(cursor.getColumnIndex(CONTACT_COLUMNS[2]));
String value = String.format(
new Locale("tr"),
"\nid : %s\n" +
"name : %s\n" +
"photoThumb : %s\n" +
"==========================================",
id,
name,
photoThumb
);
Log.i("contact", value);
}
cursor.close();
Sonuç :
I/contact: id : 9
name : ali
photoThumb : null
==========================================
id : 12
name : ayşe
photoThumb : null
==========================================
id : 17
name : çiçek
photoThumb : null
==========================================
id : 11
name : deli
photoThumb : null
==========================================
id : 13
name : fatma
photoThumb : null
==========================================
id : 14
name : hayriye
photoThumb : content://com.android.contacts/contacts/14/photo
==========================================
id : 10
name : veli
photoThumb : null
==========================================
I/contact: id : 16
name : 123456789
photoThumb : null
==========================================
id : 15
name : 987654321
photoThumb : null
==========================================
Şimdi kodları toparlayalım. Alacağımız bilgiler belli değil mi?
final String[] CONTACT_COLUMNS = {
"_id",
"display_name",
"photo_thumb_uri",
"sort_key"
};
Biz rehber kayıtlarına ait bu bilgileri alacağız. Bu bilgileri bir sınıf ile temsil etmemiz gerek.
public final class Contact {
private String name;
private String id;
private String thumbNailPhoto;
}
Bu alanlara get
metotlarını yaz. Ve kayıtları aldığımız kodları da bir sınıf içine alalım.
public class Contacts {
private final String[] CONTACT_COLUMNS = {
"_id",
"display_name",
"photo_thumb_uri",
"sort_key"
};
private Contacts() {}
public static List<Contact> getContacts(final Context context){
if(context == null) return null;
final Cursor cursor = context.getContentResolver().query(
ContactsContract.Contacts.CONTENT_URI,
CONTACT_COLUMNS,
null,
null,
"sort_key" + " asc"
);
if (cursor == null) return null;
final List<Contact> contacts = new ArrayList<>();
while (cursor.moveToNext()) {
final String id = cursor.getString(cursor.getColumnIndex(CONTACTS_COLUMNS[0]));
final String name = cursor.getString(cursor.getColumnIndex(CONTACTS_COLUMNS[1]));
final String photoThumb = cursor.getString(cursor.getColumnIndex(CONTACTS_COLUMNS[2]));
final Contact contact = new Contact(id, name, photoThumb);
contacts.add(contact);
}
cursor.close();
return contacts;
}
}
Fakat şu en baştaki diziyi de düzeltmemiz gerek. Biz böyle sütun isimlerini direk yazdık ama bu sütunların java karşılığı var.
private final String[] CONTACT_COLUMNS = {
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.PHOTO_THUMBNAIL_URI,
ContactsContract.Contacts.SORT_KEY_PRIMARY
};
Bu şekilde daha doğru oldu. Biz şimdilik tüm işlemleri onCreate
metodunda yazacağız.
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final List<Contact> contactList = Contacts.getContacts(this);
}
}
Rehber kayıtlarını aldık ama farkettiysen telefon numaraları yok. Anlayacağın mevzu daha yeni başlıyor.
Hesaplar
Telefon numaralarını almak için, tıpkı ContactsContract.Contacts.CONTENT_URI
adresi gibi başka bir adrese başvuracağız.
ContactsContract.CommonDataKinds.Phone.CONTENT_URI
Telefon numaraları ve ilgili bilgiler bu adreste. İlgili bilgiler içine yukarıda aldığımız bilgilerin hepsi dahil. Peki madem öyle biz neden direk buraya girmedik? İstersen girebilirsin. Yani rehber kayıtlarını direk buradan alabilirsin diğer adrese hiç uğramadan. Ancak bu adreste sadece numarası olan kişiler kayıtlı. Yani sadece isim ile kaydedilen kişiler burada yok. Ayrıca bu adresdeki kayıtlar her bir hesap için ayrı ayrıdır. Yani aynı numara birden fazla kez bulunabilir. Mesela telefonda Whatsapp kullanılıyorsa, bu uygulama kendine ait bir hesap ile kişilerini rehberde tutar. Duo uygulaması da öyle. Ve benzer bir çok uygulama rehberde kendine özel bir hesap ile kişileri tutar. İşte bu yüzden aynı numara birden fazla kez bulunabilir. Ayrıca simkart rehberi de bir hesaptır. Bu yüzden bu adresteki kişileri tekrarsız ve eksiksiz almak için gerekli kodları yazmalıyız. Ayrıca numarasız kayıtları da düşünürsek, neden direk bu adres değil sorusunun cevabını 80% oranında vermiş oluruz. Ancak yine de direk bu adresi kullanabiliriz, tercih meselesi.
İlk kullandığımız adreste kişiler tekti. Yani her bir kişinin kendine has bir id değeri var ve bu değer benzersiz. Bu değere sahip sadece tek bir kayıt olabilir. Aynı şey bu adres için de geçerli. Ancak bir durum var. Mesela rehberde bir kişiye iki numara kaydetmişsek, bu kişi bu adreste her bir numara için farklı id ile geçer. Ama contact_id
'leri aynıdır. Bu arada hemen hemen her telefonda bulunan google hesabını söylemeyi unutmuşum. Bu google hesabı aslında telefonda kullanılan ana hesaptır ve birden fazla da olabilir. Ve genellikle rehber kayıtları bu hesaba kaydedilir. Yani yeni bir kişi kaydederken bu hesaba kaydederiz. Eğer telefonun ana hesabını bulabilirsek bu adrese direk sorgu yaparken sorguyu bu ana hesaptaki kayıtları alacak şekilde özelleştirerek temiz bir şekilde tüm kayıtları numaralarıyla birlikte alabiliriz.
Peki telefondaki hesapları nasıl alabiliriz?
public static Account[] getAccounts(final Context context){
if(context == null) return null;
AccountManager manager = (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
if(manager == null) return null;
return manager.getAccounts();
}
Tüm hesapları bu şekilde alabiliriz. Bu hesapları rehber kayıtlarını alırken kullanabiliriz. Yani her bir hesaba ait kayıtları ayrı ayrı alabiliriz.
ContactsContract
Telefon rehberi olayının merkezinde
ContactsContract
sınıfı bulunmakta. Ve bu sınıfın içinde bir sürü sınıf ve arayüz tanımlı. Tanımlanan bu sınıf ve arayüzler, veri tabanında bilgileri türlerine göre ayırmak ve bir düzen oluşturmak için tanımlanmıştır. Biz buraya kadar bu sınıfın içindeki Contacts
sınıfını kullandık. Aldığımız bilgiler çok kısıtlı idi. Sadece 4 sütuna ait bilgileri aldık ama bundan çok daha fazla sütun olduğunu da gördük. Şimdi ContactsContract.CommonDataKinds.Phone.CONTENT_URI
aderesindeki sütunlara bakalım.
Bu arada android, telefon rehberi için bu adreslerin dışında adresler de sağlar. Mesela,
ContactsContract.RawContacts.CONTENT_URI
ContactsContract.Data.CONTENT_URI
Şimdi bir fonksiyon yazalım ve verilen adreste hangi sütunların olduğunu bize söylesin.
public static String[] getColumns(final Context context, final Uri uri) {
if(context == null || uri == null) return null;
final Cursor cursor = context.getContentResolver().query(uri,null,null,null,null);
if (cursor == null) return null;
final String[] columns = cursor.getColumnNames();
cursor.close();
return columns;
}
Şimdi de bu fonksiyonu ContactsContract.CommonDataKinds.Phone.CONTENT_URI
adresi için kullanalım.
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final String[] columns = Contacts.getColumns(this, ContactsContract.CommonDataKinds.Phone.CONTENT_URI);
if (columns != null) {
for (String column : columns) {
Log.i("columns", column);
}
}
}
}
Sonuç :
I/columns: phonetic_name
status_res_package
custom_ringtone
contact_status_ts
account_type
data_version
photo_file_id
contact_status_res_package
group_sourceid
display_name_alt
sort_key_alt
mode
last_time_used
starred
contact_status_label
has_phone_number
chat_capability
raw_contact_id
carrier_presence
contact_last_updated_timestamp
res_package
photo_uri
data_sync4
phonebook_bucket
times_used
display_name
sort_key
data_sync1
version
data_sync2
data_sync3
photo_thumb_uri
status_label
contact_presence
in_default_directory
times_contacted
_id
account_type_and_data_set
name_raw_contact_id
status
phonebook_bucket_alt
last_time_contacted
pinned
is_primary
photo_id
contact_id
contact_chat_capability
contact_status_icon
in_visible_group
phonebook_label
account_name
display_name_source
data9
dirty
sourceid
phonetic_name_style
send_to_voicemail
data8
lookup
data7
data6
phonebook_label_alt
data5
is_super_primary
data4
data3
data2
data1
data_set
contact_status
backup_id
preferred_phone_account_component_name
raw_contact_is_user_profile
status_ts
data10
preferred_phone_account_id
data12
mimetype
status_icon
data11
data14
data13
hash_id
data15
Bu sütunlar içinde en önemlisi contact_id
sütunu. Çünkü bu sütun ContactsContract.Contacts.CONTENT_URI
adresindeki _id
sütunu ile aynı. Yani bu sütunlar iki adres arasında bir köprü gibi. Yukarıda az önce veya biraz önce ya da bir kaç saat veya yıl önce demiştikki, _id sütunu benzersizdir. Üstüne de bir tür benzerlikten bahseder gibi olduk. Android dışındakileri bilmem ama android sisteminde uğraşacağımız veri tabanlarının tamamında _id sütunu benzersizdir. Ancak telefon rehberi konusunda şöyle bir durum var. Şu ki, android sistemi rehber konusunu geniş kapsamlı ele almakta ve contact_id sütunu rehberle ilgili kullanacağımız, ziyaret edeceğimiz adresler arasında bir köprüdür. Yani buradaki _id bu adrese özeldir. Ama contact_id kişi kayıtları ile ilgili bağlantıları kurmak için kullanılır. Bu da şu demek, ContactsContract.Contacts.CONTENT_URI
adresinden alacağımız _id değeri ile ContactsContract.CommonDataKinds.Phone.CONTENT_URI
adresindeki (veya diğer adreslerdeki) contact_id değeri baş göz edilmiş durumdadır ve bu kişinin farklı bilgilerine bu yolla ulaşabiliriz. Başa saralım, ContactsContract.CommonDataKinds.Phone.CONTENT_URI
adresinin de _id
sütunu var. Ve bu sütun da bu adreste benzersizdir. Yani her bir kayıt için tektir. Ancak contact_id
tek değil, birden fazla olabilir. Yani aynı contact_id değerine sahip birden fazla kayıt olabilir. Bu adresteki _id
değeri, telefondaki hesaplara göre her bir kaydı temsil eder. Yani bir kişiye ait telefon numarası ile aynı kişiye ait whatsapp numarası farklı _id değerleri ile gelir. Yani numara aynı ama _id
farklı. Aynı zamanda contact_id
de farklı. Ama kişinin iki numarası varsa, yine farklı _id
ve ama aynı contact_id
ile görünür.
Bu adreste bir diğer önemli sütun ise account_name
sütunu. Bu sütun, kişinin hangi hesap altında kayıtlı olduğunu gösterir. Eğer telefonda hiçbir hesap yoksa bu alan tabiki null
'dır. Ancak android telefonlarda GooglePlay çok popüler ve çok gerekli olduğu için bir telefonda mutlaka ama mutlaka bir tane google hesabı olur. Çünkü GooglePlay kullanmak için bir google hesabı ile giriş yapılması zorunlu. Ve bu giriş, telefon rehberine kesinlikle yansıyor. Ve yeni kaydedilen rehber kayıtları bu hesaba kaydediliyor. Tabi bunu değiştirmek de mümkün. Mesela telefon hafızası yada simkart hesabı kullanılabilir. Ancak google hesabına kaydedilmesi şiddetle tavsiye edilir. Çünkü nereye gidersen git, ne telefon kullanırsan kullan, aynı hesap ile giriş yaptığında, daha önce kayıtlı tüm kişilerin rehbere hemen gelir. Kişilerin google hesabına kaydedildiğini varsayarsak, account_name
alanında bu hesap yazar. Kişinin ayrıca bir whatsapp hesabı varsa bu da ayrı bir kayıt olarak tutulur ve account_name
alanında WhatsApp yazar (aynı numara ile). Veya başka bir hesap varsa o yazar. Ve biz bu şekilde, belirli bir hesaba ait kişileri almak istersek sorgumuzu bu yönde özelleştirebiliriz. Bunu birazdan yapacağız.
Farkettiysen bu adresteki sütunlar içerisinde de numara bilgisini göremedik. Aslında var. Ama number
adında bir sütun ile değil. Zaten böyle olması biraz zor çünkü kişinin birden fazla numarası olabilir ve bu numaralar ev iş cep falan gibi farklı türde olabilir. Bu yüzden tek bir number
sütunu bu işi göremez. Numara bilgisi data1
sütununda kayıtlı. Aslında bu alan ContactsContract.Data.CONTENT_URI
adresinde olan bir alan. ContactsContract.Data sınıfı biraz üstü örtülü bir yapıya sahip. Burada data1-data15
arası alanlar var ve bu alanlardaki bilgiler, bilginin türüne göre farklılık gösterir. Ancak bu Data
sınıfı biraz kafa karıştırıcı olabilir. Bu yüzden bu konuya hiç dokunmayalım. Zaten kimse bu alanlara doğrudan bakmaz. CommonDataKinds.Phone
sınıfı da gerekli olan dolaylı bakışı sağlar.
ContactsContract.CommonDataKinds.Phone.NUMBER
NUMBER = data1
olduğunu söylersek durum anlaşılır sanırım. Telefon rehberi bölümlere ayrılmış ve gerekli bilgileri bu bölümlere contact_id
ile başvurarak alabiliriz. Bu aldığımız bilgilerden bazıları işte bu data
alanlarına denk geliyor. Mesela email adresi. Eğer kişinin bir email adresi varsa bu da data
alanında kayıtlıdır. Ama bunu direk data
alanlarına girerek almayız.
Aslında mevzu şöyle : telefon rehberi kişilerden oluşuyor. Ve bu telefon rehberinin ana adresi ContactsContract.Contacts.CONTENT_URI
adresidir. Yeryüzünde nasılki hiçbir kişi başka bir kişiyle aynı değil ise, bu adreste de her kişi diğerinden farklı bir kişidir. Yani bu adreste olay kişi olayıdır. Bu yüzden bu ana adresten aldığımız her kayıt ayrı bir kişidir. Tekrar etmez. Kişinin ismi veya numarası veya her ikisi başka bir kişiyle aynı olsa bile bunlar iki ayrı kişidir. Ve bu adres kişi ile ilgili bir anlamda genel bilgileri içerir. Bize kişinin telefon numarasını veya email adresini direk vermez. Bu bilgileri almak için buradan _id
değerini alırız ve diğer adreslerdeki contact_id
değeri üzerinden elde ederiz. Yani bu adres, diğer adreslerle contact_id
üzerinden ilişkilendirilmiştir. Biraz kod yazalım da beynimize kan gitsin.
Öncelikle yeni bilgiler alacağımız için Contact
sınıfımıza eklemeler yapmamız gerek. Ya da yeni bir sınıf oluşturalım.
public class Contact2 {
private final String contactId;
private final String name;
private final String number;
private final String accountName;
private final String id;
private final String email;
private final String accountType;
public Contact2(final String contactId, final String name, final String number, final String accountName, final String id, final String email, final String accountType) {
this.contactId = contactId;
this.name = name;
this.number = number;
this.accountName = accountName;
this.id = id;
this.email = email;
this.accountType = accountType;
}
@Override
public String toString() {
return String.format(
new Locale("tr"),
"id : %s\n" +
"contactId : %s\n" +
"name : %s\n" +
"number : %s\n" +
"accountName : %s\n" +
"accountType : %s\n" +
"email : %s\n" +
"================================",
id, contactId, name, number, accountName, accountType, email
);
}
}
Sınıfımızda hem contactId hem de id değerleri var. Bunların ne anlama geldiğini yukarıda anlattık. Bu iki bilgi bize farklılıklarıyla nasıl bir yapıya sahip olduklarını anlama konusunda yardımcı olacak. Ayrıca ben yeni bir rehber oluşturdum ve sade olsun diye 3 kişi kaydettim.

Bu kişilerden ali'nin iki telefon numarası ve bir tane email adresi var.

Contact2
sınıfı hangi sütunları alacağımız hakkında bir fikir vermiş olmalıydı.
private static final String[] NUMBERS_COLUMNS = {
ContactsContract.CommonDataKinds.Phone._ID,
ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER,
ContactsContract.SyncState.ACCOUNT_NAME,
ContactsContract.SyncState.ACCOUNT_TYPE,
ContactsContract.CommonDataKinds.Phone.SORT_KEY_PRIMARY,
};
Alacağımız sütunlar bunlar. Burada ana adresimiz ContactsContract.CommonDataKinds.Phone.CONTENT_URI
oluyor. SyncState
bir interface ve hesap ile ilgili sütunlar burada tanımlanmış. ACCOUNT_NAME = "account_name".
Bu sütun bize kişinin hangi hesapta kayıtlı olduğunu verecek. Ancak deneme yaptığımız telefonda henüz bir hesap yok, bu yüzden bu alanlar null
gelecek şimdilik. ContactsContract.CommonDataKinds.Phone
sınıfı bu alanları hem direk tanımlamamış, hem de bu interface'i uygulamamış. Ancak biraz yukarıda da gördüğümüz gibi bu alanlar bu adreste var. ContactsContract
sınıfı içinde bu interface'i uygulayan tek bir sınıf var, ContactsContract.RawContacts
sınıfı. Yani biz yukarıda belirttiğimiz hesap ile ilgili iki sütunu bu sınıf üzerinden de belirtebiliriz.
private static final String[] NUMBERS_COLUMNS = {
ContactsContract.CommonDataKinds.Phone._ID,
ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER,
ContactsContract.RawContacts.ACCOUNT_NAME,
ContactsContract.RawContacts.ACCOUNT_TYPE,
ContactsContract.CommonDataKinds.Phone.SORT_KEY_PRIMARY,
};
Tabiki sütun isimlerini direk el ile de yazmamız mümkün.
private static final String[] NUMBERS_COLUMNS = {
ContactsContract.CommonDataKinds.Phone._ID,
ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER,
"account_name",
"account_type",
ContactsContract.CommonDataKinds.Phone.SORT_KEY_PRIMARY,
};
Ama biz bir öncekini tercih edelim. Ve bilgileri alacak fonksiyonu yazalım.
public static List<Contact2> getContacts2(final Context context) {
if (context == null) return null;
final Cursor cursor = context.getContentResolver().query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
NUMBERS_COLUMNS,
null,
null,
NUMBERS_COLUMNS[6] + " asc"
);
if (cursor == null) return null;
List<Contact2> contactList = new ArrayList<>();
while (cursor.moveToNext()) {
final String id = cursor.getString(cursor.getColumnIndex(NUMBERS_COLUMNS[0]));
final String contactId = cursor.getString(cursor.getColumnIndex(NUMBERS_COLUMNS[1]));
final String name = cursor.getString(cursor.getColumnIndex(NUMBERS_COLUMNS[2]));
final String number = cursor.getString(cursor.getColumnIndex(NUMBERS_COLUMNS[3]));
final String accountName = cursor.getString(cursor.getColumnIndex(NUMBERS_COLUMNS[4]));
final String accountType = cursor.getString(cursor.getColumnIndex(NUMBERS_COLUMNS[5]));
String email = "null";
final Cursor cursor1 = context.getContentResolver().query(
ContactsContract.CommonDataKinds.Email.CONTENT_URI,
new String[]{ContactsContract.CommonDataKinds.Email.DATA, ContactsContract.CommonDataKinds.Email.CONTACT_ID},
ContactsContract.CommonDataKinds.Email.CONTACT_ID + "=?",
new String[]{contactId},
null
);
if (cursor1 != null) {
if (cursor1.moveToNext())
email = cursor1.getString(cursor1.getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA));
cursor1.close();
}
final Contact2 contact = new Contact2(contactId, name, number, accountName, id, email, accountType);
contactList.add(contact);
}
cursor.close();
return contactList;
}
Sonuç :
I/contacts:
id : 13
contactId : 4
name : ali
number : 05325323232
accountName : null
accountType : null
email : kahverengikahverengidir@gmail.com
================================
id : 12
contactId : 4
name : ali
number : 05455454545
accountName : null
accountType : null
email : kahverengikahverengidir@gmail.com
================================
id : 18
contactId : 6
name : deli
number : (085) 025-8852
accountName : null
accountType : null
email : null
================================
id : 16
contactId : 5
name : veli
number : 08005258987
accountName : null
accountType : null
email : null
================================
Kişinin email adresini bulmak için contact_id
sütununu kullandık.
String email = "null";
final Cursor cursor1 = context.getContentResolver().query(
ContactsContract.CommonDataKinds.Email.CONTENT_URI,
new String[]{ContactsContract.CommonDataKinds.Email.DATA, ContactsContract.CommonDataKinds.Email.CONTACT_ID},
ContactsContract.CommonDataKinds.Email.CONTACT_ID + "=?",
new String[]{contactId},
null
);
if (cursor1 != null) {
if (cursor1.moveToNext())
email = cursor1.getString(cursor1.getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA));
cursor1.close();
}
Ancak bu contact_id
değerini nereden aldığımıza dikkat et. Aslında bunu ilk kullandığımız adresten de alabileceğimizi biliyorsun. Zaten bu contact_id
'nin kaynağı ContactsContract.Contacts.CONTENT_URI
adresi. Yukarıda ne demiştik, ContactsContract.CommonDataKinds.Phone.CONTENT_URI
adresinde kişiler her bir numara için tekrar eder. ali kişisinin iki numarası olduğu için bunlar ayrı ayrı geldi. _id
'ler farklı ama contact_id
'ler aynı.
Ancak dikkat edilmesi gereken şey, bu bilginin, yani email adresinin birden fazla olabileceği. Yukarıdaki kodda, karşımıza çıkan ilk email adresinini aldık. Aslında orada bir döngü olmalıydı ve tüm email adreslerini alabilecek şekilde yazılmalıydı.
final List<String> emails = new ArrayList<>();
if (cursor1 != null) {
while (cursor1.moveToNext())
emails.add(cursor1.getString(cursor1.getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA)));
cursor1.close();
}
İşte bu mevzu aynen numaralar (veya benzer alanlar) için de geçerli. Ancak biz burada zaten numaraların olduğu adrese sorgu yaptık. Yani burada zaten hepsi ayrı ayrı geliyor. Yani numaraları bir diziye atmanın bir gereği yok. Ancak ContactsContract.Contacts.CONTENT_URI
adresinden _id
'yi alıp buraya contact_id
üzerinden başvursaydık numaraları almak için, aynı yukarıdaki gibi bir döngü kurmamız gerekecekti. Neden? Çünkü bir kişiye birden fazla numara kaydetmek mümkün.
Şimdi kodları değiştirmeden önce hesap bilgilerini de görelim. Ben telefonda bir google hesabı açıp aynı kodları tekrar çalıştırıyorum. Ve sonuç :
I/contacts: id : 13
contactId : 4
name : ali
number : 05325323232
accountName : kahverengikahverengidir@gmail.com
accountType : com.google
email : kahverengikahverengidir@gmail.com
================================
id : 12
contactId : 4
name : ali
number : 05455454545
accountName : kahverengikahverengidir@gmail.com
accountType : com.google
email : kahverengikahverengidir@gmail.com
================================
I/contacts: id : 18
contactId : 6
name : deli
number : (085) 025-8852
accountName : kahverengikahverengidir@gmail.com
accountType : com.google
email : null
================================
I/contacts: id : 16
contactId : 5
name : veli
number : 08005258987
accountName : kahverengikahverengidir@gmail.com
accountType : com.google
email : null
================================
Google hesabı için account_type
com.google olarak geliyor. account_name
ise direk hesabı veriyor. Eğer whatsapp hesabı olsaydı account_type
com.whatsapp olarak gelirdi, account_name
ise WhatsApp olurdu. Eğer simkart hesabı olsaydı account_type
com.android.sim, account_name
ise SIM1 olurdu. Ya da şöyle yapalım,
account_type | account_name |
---|---|
com.google | xyz@gmail.com |
com.whatsapp | |
com.android.sim | SIM1 |
com.google.android.gms.matchstick | Duo |
Burada sadece google hesabının account_name
değeri değişkendir, diğerleri sabittir değişmez. Aslında şöyle bir şey de var, biz mazinin bir yerinde telefondaki tüm hesapları almıştık değil mi? Evet almıştık. Ancak hesapları aldığımız o fonksiyon bize telefondaki tüm hesapları veriyor. Tüm hesaplar telefon rehberi ile alakalı olmayabilir. Mesela ben kendi telefonumda myMail uygulaması kullanıyorum ve bu uygulamanın telefon rehberiyle bir ilgisi yok. Ancak mailleri alabilmek için mutlaka bir mail adresiyle giriş yapmak gerekiyor. İşte bu giriş bir hesap olarak telefona kaydediliyor. Ama rehberle bir ilgisi yok. Bunun gibi bir çok uygulama var. Fakat yine de bu hesapları rehberde sorgu yaparken kullanabiliriz. Yani hata vermez. Çünkü sonuçta bu bir sorgu, şu kişi rehberde var mı diye sorduğumuzda varsa var yoksa yok, hata almayız.
Mesela şimdi google hesaplarına ait kayıtları alalım. Google hesaplarının account_type
değerini öğrendik nasılsa.
final Cursor cursor = context.getContentResolver().query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
NUMBERS_COLUMNS,
NUMBERS_COLUMNS[5] + "=?",
new String[]{"com.google"},
NUMBERS_COLUMNS[6] + " asc"
);
NUMBERS_COLUMNS[5] = ContactsContract.RawContacts.ACCOUNT_TYPE
. Anladın sen onu. Burada query
metodunun selection ve selectionArgs parametrelerini belirttik. selection parametresinin türü String
, selectionArgs ise String[]
. Bu şekilde bir sorgu bize rehberdeki sadece google hesaplarına ait kişileri verir. Birden fazla google hesabı olabileceğini unutma. Peki eğer direk belirli bir google hesabına ait kişileri almak istersek?
final Cursor cursor = context.getContentResolver().query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
NUMBERS_COLUMNS,
NUMBERS_COLUMNS[4] + "=?",
new String[]{"falan_filan@gmail.com"},
NUMBERS_COLUMNS[6] + " asc"
);
NUMBERS_COLUMNS[4] = ContactsContract.RawContacts.ACCOUNT_NAME
oluyor.
Bu arada sorgu yaptığımız adrese dikkat. Bu tür sorguları, yani hesap ismi falan gibi sorguları ContactsContract.CommonDataKinds.Phone.CONTENT_URI
adresine yapıyoruz. Çünkü bu sütunlar burada. Ama sene 1342'de de söylediğim gibi, genel yaklaşım ilk önce ContactsContract.Contacts.CONTENT_URI
adresine gitmek. Buradan aldığımız _id
değerleriyle diğer bilgileri ordan burdan contact_id
eşleşmesiyle bulup getirmek. Android sisteminin yerleşik rehber uygulamasının da yaptığı tam olarak budur. Rehberi açtığında ilk önce isimler, birer küçük resimle gösterilir. Bir kişiye dokunduğunda detay ekranı açılır. Aslında güzel bir tasarım. Ama sen daha farklı bir tasarım yapabilirsin. Mesela rehber açılır açılmaz tüm kişiler tüm detaylarıyla görünsün isteyebilirsin falan filan. Ama bu material tasarımı bozabilir çünkü material tasarımın temeli sadeliktir. Ama nedense millet sadece renklere kafayı takmıştır. Yani material renklere. Oysa asıl önemli olan sadelik.
Contacts
Şimdi biz de android tasarımına benzer bir rehber yapalım. Ve bu rehberi birlikte geliştirelim. Bugün sadece giriş kısmını yazacağız. Uygulamamızın tasarımını başta android'in rehber tasarımına benzetmeye çalışacağız. Elbette tıpatıp aynısı olmayacak. Ama tasarımın çoğunu ondan örnek alacağız. Mesela android rehberi, resmi olmayan kişilerin baş harfini şekilli bir resim yaparak koyuyor, biz de öyle yapacağız. Bu iş için TextDrawable sınıfını kullanacağız. Bu sınıf, arkaplan rengini belirleyebileceğimiz hem dairesel hem köşeli textview'lar üretiyor. Yani yazısını da biz belirliyoruz. Textview diyorum ama aslında bu tam olarak bir textview değil, çizim. Yani bir imageview'a çizim olarak atanıyor. Bu çizim, tek renk bir arkaplan ve bu arkaplan üzerine yazılmış bir yazıdan oluşuyor. Biz burada android tasarımına uymayı ve dairesel view'lar kulanmayı tercih ettik. Ve yine android tasarımına uyarak, resmi olmayan kişilerin sadece baş harflerini göstereceğiz. TextDrawable sınıfı ile farklı şeyler de yapılabilir. Uygulamamızın android tasarımından en büyük ve en ayırdedici farkı ana renkleri olacak. Biz kendi renklerimizi kullanacağız.
Ana ekran.

Sahnemiz bu. Kişilerin yanısıra arama kayıtları da olacak uygulamamızda ama şimdi sadece kişiler. Bu ana ekrana bir ViewPager
koyacağız ve viewPager iki sayfadan oluşacak, biri kişiler, diğeri arama kayıtları. Android bunlara ek bir de favoriler kısmı eklemiş ama biz favori olayına hiç girmeyeceğiz. Uygulamamız açıldığında direk kişiler çıkacak, sağdan sola doğru kaydırıldığında da arama kayıtları çıkacak.
Contact
sınıfımız şöyle :
public class Contact{
private final String id;
private final String name;
private final Uri thumbNailPhoto;
public Contact(String id, String name, Uri thumbNailPhoto) {
this.id = id;
this.name = name;
this.thumbNailPhoto = thumbNailPhoto;
}
public String getId() { return id; }
public String getName() { return name; }
public Uri getThumbNailPhoto() { return thumbNailPhoto; }
@Override
public String toString() {
return String.format(
new Locale("tr"),
"id : %s\n" +
"name : %s\n" +
"photo : %s\n" +
"================================",
id, name, thumbNailPhoto
);
}
}
Görüldüğü üzere sadece isim, id ve küçük resmi alacağız ilk açılışta. Yani kişiler sayfamız android tasarımına tam uygun olacak. Burada bizim farkımız TextDrawable sınıfı olacak ve görünüm biraz daha iyi olacak. Görünüm ve renkler konusunda material tasarıma mümkün olduğunca uyacağız.
Proje Telefon Rehberi
Projemizin dosya yapısı doğru mu oldu tam emin değilim.

Ancak bu benim kolayıma gidiyor. Neyi nereye koyduğumu rahatlıkla bulabiliyorum.

Şimdilik tek bir activity'miz var. Bu da ana activity. Diğer sınıfları da yazmaya başladım ama bugün sadece kişilerle ilgileniyoruz.
public class Contacts {
private Contacts() {}
private final static String[] CONTACT_COLUMNS = {
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.PHOTO_THUMBNAIL_URI,
ContactsContract.Contacts.SORT_KEY_PRIMARY
};
@Nullable
public static List<Contact> getContacts(final Context context){
if(context == null) return null;
final Cursor cursor = context.getContentResolver().query(
ContactsContract.Contacts.CONTENT_URI,
CONTACT_COLUMNS,
null,
null,
CONTACT_COLUMNS[3] + " asc"
);
if(cursor == null) return null;
final List<Contact> contacts = new ArrayList<>();
if(cursor.getCount() == 0) return contacts;
final int idColumn = cursor.getColumnIndex(CONTACT_COLUMNS[0]);
final int nameColumn = cursor.getColumnIndex(CONTACT_COLUMNS[1]);
final int photoColumn = cursor.getColumnIndex(CONTACT_COLUMNS[2]);
while (cursor.moveToNext()) {
String photo = cursor.getString(photoColumn);
contacts.add(
new Contact(
cursor.getString(idColumn),
cursor.getString(nameColumn),
photo != null ? Uri.parse(photo) : null
));
}
cursor.close();
return contacts;
}
}
Biz bu kişileri almasına alacağız ama izinler konusunu henüz hiç konuşmadık. İzin konusu önemli. Biliyorsun rehber erişimi izne tâbi. Yani rehbere erişmek için kullanıcıdan izin almamız gerek. Burada da yine EasyPermissions kullanalım.
ViewPager içine koyacağımız iki sayfa da Fragment
olacak. Biri ContactsFragment
, diğeri CallLogFragment
. Ve izinleri her fragment kendi içinde halletsin diye düşünüyorum. Sen ne düşünüyorsun?
ContactsFragment.xml
<FrameLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="match_parent"
android:layout_width="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android" >
<com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:fastScrollAutoHide="false"
app:fastScrollPopupBgColor="@color/colorPrimary"
app:fastScrollPopupTextColor="@color/white"
app:fastScrollThumbColor="@color/colorPrimaryDark"
app:fastScrollThumbEnabled="true"
app:fastScrollTrackColor="@color/transparent"
tools:context=".fragments.ContactsFragment"
tools:listitem="@layout/contact_item" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone" />
</FrameLayout>
İlk sayfamız kişiler sayfası. Kişileri listelemek için FastScrollRecyclerView sınıfını kullanıyoruz. Ve bu listede her bir kişinin gösterileceği view ise şöyle :
contact_item.xml
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="@dimen/contact_item_layout_height"
android:orientation="horizontal"
android:background="@drawable/ripple"
android:clickable="true"
android:focusable="true">
<android.support.v7.widget.CardView
android:layout_width="@dimen/contact_item_image_width"
android:layout_height="@dimen/contact_item_image_width"
android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/contact_item_card_margin_start"
android:shape="ring"
app:cardCornerRadius="@dimen/contact_item_card_corner_radius"
app:cardElevation="@dimen/contact_item_card_elevation"
app:contentPadding="@dimen/contact_item_card_content_padding">
<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.v7.widget.CardView>
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:maxLines="1"
tools:text="contact name"
style="@style/ContactText"/>
</LinearLayout>
Birazdan ne olduğunu göreceksin. TextDrawable sınıfı imageview'ı kendisi dairesel yapıyor. Ancak resmi olan kişilerin de küçük resminin dairesel görünmesi için imageview'ı cardview içine koyduk. Cardview dairesel. Bu item için adapter da yazdık tabiki.
public class ContactAdapter
extends RecyclerView.Adapter<ContactAdapter.ViewHolder>
implements FastScrollRecyclerView.SectionedAdapter {
private final List<Contact> contacts;
private static final ColorGenerator colorGenerator = ColorGenerator.MATERIAL;
private ContactSelectListener contactSelectListener;
public ContactAdapter(@NotNull final List<Contact> contacts){
this.contacts = contacts;
}
public ContactAdapter setContactSelectListener(@NotNull final ContactSelectListener contactSelectListener){
this.contactSelectListener = contactSelectListener;
return this;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.contact_item, parent, false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
final Contact contact = contacts.get(position);
holder.name.setText(contact.getName());
if (contact.getThumbNailPhoto() == null) {
Drawable drawable = TextDrawable.builder().buildRound(contact.getName().substring(0, 1).toLowerCase(), colorGenerator.getRandomColor());;
holder.image.setImageDrawable(drawable);
return;
}
holder.image.setImageURI(contact.getThumbNailPhoto());
}
@Override public int getItemCount() { return contacts.size(); }
@NonNull
@Override
public String getSectionName(int position) {
return contacts.get(position).getName().substring(0,1);
}
final class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{
final TextView name;
final ImageView image;
ViewHolder(@NonNull View itemView) {
super(itemView);
name = itemView.findViewById(R.id.name);
image = itemView.findViewById(R.id.image);
itemView.setOnClickListener(this);
}
@Override public void onClick(View view) { onClicked(view, getAdapterPosition()); }
private void onClicked(final View view, final int position) {
if(contactSelectListener != null) contactSelectListener.onContactSelect(view, position);
}
}
}
Burada kullandığımız interface :
public interface ContactSelectListener {
void onContactSelect(final View view, final int index);
}
Bunu dokunma olayında kişiyi almak için kullanıyoruz.
Şimdi sıra geldi ContactsFragment sınıfını yazmaya.
public class ContactsFragment
extends Fragment
implements ContactSelectListener {
public ContactsFragment() {
// Required empty public constructor
}
public static final Logger log = Logger.jLog();
private List<Contact> contacts;
private FastScrollRecyclerView recyclerView;
private static final int RC_CONTACTS = 2;
private static final String[] CONTACTS_PERMISSIONS = {Manifest.permission.READ_CONTACTS};
@Override
public View onCreateView(@NotNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_contact_list, container, false);
recyclerView = view.findViewById(R.id.recyclerView);
LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
recyclerView.setLayoutManager(layoutManager);
Run.run(this::start, 100);
return view;
}
private void start(){
if(getContext() == null) return;
if(EasyPermissions.hasPermissions(getContext(), CONTACTS_PERMISSIONS)){
onPermissionsGranted();
return;
}
EasyPermissions.requestPermissions(
this,
getString(R.string.contacts_permission_rationale),
RC_CONTACTS,
CONTACTS_PERMISSIONS);
}
@AfterPermissionGranted(RC_CONTACTS)
private void onPermissionsGranted(){
log.i("onPermissionsGranted");
new Run<>(
getActivity(),
this::getContacts,
this::setContacts
);
}
private List<Contact> getContacts(){
return Contacts.getContacts(getContext());
}
private void setContacts(final List<Contact> contacts) {
if(contacts != null) recyclerView.setAdapter(new ContactAdapter(this.contacts = contacts).setContactSelectListener(this));
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
}
@Override
public void onContactSelect(View view, int index) {
Contact contact = contacts.get(index);
log.d(contact);
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, contact.getId());
intent.setData(uri);
getContext().startActivity(intent);
}
}
Log mesajları için sen ne kullanıyorsan kullan. Bu sınıf şuan izinlere bakıp kişileri almak istiyor. Fakat kişileri alırken arkaplanda çalışmamız lazım. Yani izinlere bakacağız, varsa arkaplanda kişileri alıp ön plana sahneye çıkaracağız. Eğer izin yoksa soracağız ve onay verilirse yine aynı icraatı yapacağız.
Arkaplan işlemi için Run
sınıfını kullandık. Ne işe yaradığı belli, fonksiyonun birinden aldığı dönüş değerini diğerine veriyor. Dönüş değeri veren fonksiyon arkaplanda çalışıyor. Dönüş değerini alan fonksiyon ise activity null
değilse üzerinde çalışıyor, aksi halde arkaplanda çalışıyor. Ayrıca sınıfın içindeki run
metodu arkaplanda çalışmıyor. Bunu sadece kodu geçiktirmek için kullanıyoruz.
public class Run<T> {
private final Callable<T> callable;
private final CallableCallback<T> callback;
private final Activity activity;
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
public Run(
@Nullable final Activity activity,
@NonNull final Callable<T> callable,
@NonNull final CallableCallback<T> callback) {
this.callable = callable;
this.callback = callback;
this.activity = activity;
new Thread(this::run).start();
}
private void run() {
Future<T> future = executorService.submit(callable);
T returnValue = null;
try {
returnValue = future.get();
}
catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
executorService.shutdown();
if (activity != null) {
T finalReturnValue = returnValue;
activity.runOnUiThread(() -> callback.call(finalReturnValue));
}
else {
callback.call(returnValue);
}
}
public interface CallableCallback<T> {
void call(@Nullable T returnValue);
}
public static void run(Runnable runnable, long delay) {
new Handler().postDelayed(runnable, delay);
}
}
Burada şuan herşeyi cart diye yazıp bitiremeyeceğimiz için bazı önemli işlevleri android rehberine devredeceğiz. Mesela detay ekranını şimdi yazmıyoruz ve ekranda bir kişiye dokunulduğunda android rehberinin detay ekranı açılacak.
@Override
public void onContactSelect(View view, int index) {
Contact contact = contacts.get(index);
log.d(contact);
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, contact.getId());
intent.setData(uri);
getContext().startActivity(intent);
}
Sadece id değeri ile kişiye ait detay ekranını android'den rica ediyoruz. O da bizi kırmıyor. Ayrıca yeni kişi ekleme olayını da android'e devrediyoruz. Kişi ekleme olayını FloatingActionButton ile yapacağız ama farkettiysen bu view ana activity'de. Gerçi nerden farkedeceksin, daha görmedinki.
activity_main.xml
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.tr.hsyn.telefonrehberi.activities.MainActivity">
<android.support.design.widget.FloatingActionButton
android:id="@+id/floatingActionButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
app:layout_anchor="@+id/mainFrameLayout"
app:layout_anchorGravity="right|bottom"
app:srcCompat="@drawable/add_contact"
android:layout_marginBottom="@dimen/floating_button_bottom_margin"
android:layout_marginEnd="@dimen/floating_button_end_margin"
android:focusable="true"/>
<!--<com.xyz.systemsetting.ui.searchview.MaterialSearchView
android:id="@+id/sv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="5dip" />-->
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_gravity="fill"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:title="@string/app_name" />
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabIndicatorHeight="@dimen/tab_indicator_height">
<android.support.design.widget.TabItem
android:id="@+id/tabContacts"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/ic_people_24dp" />
<android.support.design.widget.TabItem
android:id="@+id/tabCallLog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/ic_call_history_24dp" />
</android.support.design.widget.TabLayout>
</android.support.design.widget.AppBarLayout>
<android.support.design.widget.CoordinatorLayout
android:id="@+id/mainFrameLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<android.support.v4.view.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.design.widget.CoordinatorLayout>
</android.support.design.widget.CoordinatorLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity {
private static final Logger log = Logger.jLog();
private ContactsFragment contactsFragment;
//private CallLogFragment callLogFragment;
private final List<ActionClickListener> actionClickListeners = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ViewPager viewPager = findViewById(R.id.viewPager);
final Fragment[] fragments = new Fragment[]{contactsFragment = new ContactsFragment()};
PageAdapter pageAdapter = new PageAdapter(getSupportFragmentManager(), fragments);
viewPager.setAdapter(pageAdapter);
FloatingActionButton actionButton = findViewById(R.id.floatingActionButton);
actionButton.setOnClickListener(this::onActionClick);
addActionClickListener(contactsFragment);
}
private void addActionClickListener(@NotNull final ActionClickListener actionClickListener) {
if(!actionClickListeners.contains(actionClickListener)) actionClickListeners.add(actionClickListener);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main_activity_menu, menu);
return true;
}
public void onActionClick(final View view) {
log.w("onActionClick");
for (ActionClickListener actionClickListener : actionClickListeners) {
actionClickListener.onActionClick(view);
}
}
}
Şimdi farketmişsindir FloatingActionButton ana activity'de. Bunun sebebi, bu FloatingActionButton'ı diğer arama kaydı sayfasında da kullanmak. Aslında henüz kesin birşey yok ama bu şekilde yapmak onunla oynamayı daha da kolaylaştırıyor. Bu butonun tıklanma olayına dinleyici ekleyebildiğimiz gibi, çıkarma da yapabilmemiz gerekebilir, bunu da ekleyebiliriz sonra. Bu arada kullandığımız interface şöyle,
public interface ActionClickListener {
void onActionClick(@NotNull final View view);
}
Bu durumda ContactsFragment sınıfına da bazı eklemeler yapmamız gerekiyor. Çünkü biz bu sınıfı ana activity'de ActionClickListener olarak kabul ettik.
addActionClickListener(contactsFragment);
Yani bu sınıfın bu arayüzü uygulaması gerek.
ContactsFragment.java
@Override
public void onActionClick(@NotNull View view) {
Intent intent = new Intent(ContactsContract.Intents.Insert.ACTION);
intent.setType(ContactsContract.RawContacts.CONTENT_TYPE);
intent.putExtra("finishActivityOnSaveCompleted", true);
startActivityForResult(intent, ADD_CONTACT);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode){
case ADD_CONTACT:
if (resultCode == Activity.RESULT_OK) {
start();
}
break;
}
}
Kişiler sayfasında FloatingActionButton'a tıklandığında yeni kişi ekleme sayfası açılacak. Bu açılan sayfada yeni kişi eklenebilir veya ekleme yapmadan kapatılabilir. Yani işlem iptal edilebilir. Kayıt yapılıp yapılmadığını onActivityResult metodundan öğreniyoruz. Eğer ekleme yapıldıysa kişileri yeniden yüklüyoruz. Ancak bu sadece şimdilik böyle, daha sonra değiştirebiliriz. Gerçi kişi ekleme olayını değiştirmeyiz ama detay sayfasını değiştireceğiz. Daha doğrusu kendimiz yeni bir detay sayfası tasarlayacağız. Bugün buradaki tek amacımız kişileri yüklemek ve görüntülemekti. Geri kalan işlemleri geçici olarak android'e devrettik.

Fakat bazı renler konusunda tam emin olamadım. Arada biraz değiştirip deniyorum. Mesela FastScrollRecyclerView'ın fast olayındaki renkler.

FloatingActionButton'ın arkaplan rengi otomatik olarak colorAccend değerine bağlanıyor. Yukarıda fastScroll'un renklerini de colorAccend'e bağladım. Ayrıca tabların altındaki çizgi, yani tabIndicator da colorAccend'e bağlı. Yani colorAccend değişince hepsi birden değişiyor. Buraya uygun bir renk seçmemiz gerek.
Ayrıca fast olayı başladığında FloatingActionButton'ı kaybedebiliriz.

FloatingActionButton'ın yukarı doğru yuvarlanarak kaybolmasının sebebi, daha sonra ekleyeceğemiz SnackBar mesajları. Bu mesajlar ekrana girdiğinde FloatingActionButton yukarı yuvarlanacak, ama kaybolmayacak. Bu harekete uyumluluk için fast olayında da yukarı yuvarladık. Ayrıca yine aynı şekilde arama kayıtlarına geçerken de FloatingActionButton'ı yuvarlayarak kaybedelim. Çünkü henüz arama kayıtlarında FloatingActionButton'ı' nasıl kullanacağımıza karar vermedik.

Bu olayları işlemek için takip edeceğimiz yöntem, fragment ile activity arasında interface'ler ile köprü kurmak. Aslında bildiğin gibi bu iletişim direk aracısız da yapılabilir. Mesela fragment içinde getActivity()
metodu Activity'yi verir. Bunu kendi activity'mize cast ederek (MainActivity) activity'nin tüm metotlarını kullanabiliriz. İtiraf ediyorum, başlarda ben böyle yapıyordum. Hatta bu adamlar ne diye interface tanımlıyor, direk cart diye kullansana be kardeşim diyerek çıkışıyordum. Ancak bir süre sonra kodlarda değişiklik yaptığında herşeyin boka sardığını farkettim. Ayrıca neyin ne amaçla kullanıldığını daha belirgin bir şekilde vurgulamak gerekiyor. Aslında kodların içine girmek gerekiyor. Ne yaptığını kesin olarak bilmen ve tanımlaman gerekiyor. Yani yapısal kodlaman gerekiyor. Elbette bu şekilde de yapsan değişiklikler bunu da etkiler. Ama inan bu şekilde daha kolay başediyorsun karışıklıkla. Evet kafamızda bir plan var ama daha tam kesin değil herşey. Herşey değişebilir. Kesin olsa bile yarın neyle karşılaşacağını bilmiyorsun. Yani değişiklik yapmak bazen kaçınılmaz oluyor. Bu sebeplerden dolayı işimizi düzgün yapmalıyız.
İlk önce fastScroll olayı başladığında FloatingActionButton'ı' nasıl kaybedeceğiz ona bakalım.
Peki FloatingActionButton nerede? Ana activity'de. O zaman fragment'ların veya herhangi başka bir nesnenin bu FloatingActionButton ile etkileşime geçebilmesi için bir interface tanımlamamız gerek. Ya da fragment içinde fast olayı başladığında bunu dinleyicilere bildirmemiz gerek. Yani tepkiyi ya activity içinde vereceğiz ya da fragment içinde. Biz bu ikinci seçeneği yapalım. Fast olayı başladığında bunu duyuralım. Bu olayı activity dinleyecek ve tepki verecek. Aslında bu ikinci yolu seçmemiz daha doğru çünkü FloatingActionButton activity'de. Bizim ContactsFragment sınıfımızda RecyclerView nesnemiz aslında FastScrollRecyclerView. Bu sınıfı fast olayına imkan vermesi için seçtik zaten. Android'in kendi telefon rehberinde de bu olay var biliyorsun. Gerçi ondaki biraz daha farklı. Mesela ekranın sağında fast olayı gerçekleşirken, solunda da başka bir olay gerçekleşiyor, harfler atlıyor. Biz bunu yapmıyoruz. Aslında bunun fast olayıyla bir ilgisi yok, listeyi kaydırarak da hareket ettirsek o harfler atlıyor. Daha doğrusu kayıyor.
Kullandığımız FastScrollRecyclerView sınıfı sırf bu fastScroll olayını takip edebilmemiz için bize bir interface sağlıyor.
recyclerView.setStateChangeListener(new OnFastScrollStateChangeListener() {
@Override
public void onFastScrollStart() {
//start
}
@Override
public void onFastScrollStop() {
//stop
}
});
Bu olayın dinleyicilerini iki metot tanımlamak zorunda bırakacağız.
public interface FastScrollListener {
void onFastScrollStart();
void onFastScrollStop();
}
Fakat bu olayları duyururken tek bir metot kullanalım.
private void notifyFastScrollListeners(final boolean start){}
Yani :
recyclerView.setStateChangeListener(new OnFastScrollStateChangeListener() {
@Override
public void onFastScrollStart() {
notifyFastScrollListeners(true);
}
@Override
public void onFastScrollStop() {
notifyFastScrollListeners(false);
}
});
Bu olayı birden fazla dinleyici için yayınlayacağız.
private final List<FastScrollListener> fastScrollListeners = new ArrayList<>();
public void addFastScrollListener(final FastScrollListener fastScrollListener) {
if (!fastScrollListeners.contains(fastScrollListener))
fastScrollListeners.add(fastScrollListener);
}
Bu durumda dinleyicileri nasıl uyaracağımız da belli olmuş oluyor.
private void notifyFastScrollListeners(final boolean start) {
for (FastScrollListener fastScrollListener : fastScrollListeners)
if (fastScrollListener != null)
if (start) {
fastScrollListener.onFastScrollStart();
}
else {
fastScrollListener.onFastScrollStop();
}
}
Biz ana activity'de ContactsFragment'ı örneklemiştik.
MainActivity.java
private static final Logger log = Logger.jLog();
private ContactsFragment contactsFragment;
private CallLogFragment callLogFragment;
private final List<ActionClickListener> actionClickListeners = new ArrayList<>();
private FloatingActionButton actionButton;
private PageChangeListener[] pageChangeListeners;
private ViewPager viewPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
viewPager = findViewById(R.id.viewPager);
contactsFragment = new ContactsFragment();
callLogFragment = new CallLogFragment();
contactsFragment.addFastScrollListener(this);
pageChangeListeners = new PageChangeListener[]{callLogFragment, contactsFragment};
final Fragment[] fragments = new Fragment[]{contactsFragment, callLogFragment};
PageAdapter pageAdapter = new PageAdapter(getSupportFragmentManager(), fragments);
viewPager.setAdapter(pageAdapter);
viewPager.addOnPageChangeListener(this);
actionButton = findViewById(R.id.floatingActionButton);
actionButton.setOnClickListener(this::onActionClick);
addActionClickListener(contactsFragment);
TabLayout tabLayout = findViewById(R.id.tabs);
viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
tabLayout.addOnTabSelectedListener(new TabLayout.ViewPagerOnTabSelectedListener(viewPager));
}
Artık arayüzün metotlarını uygulayıp gerekli tepkiyi verebiliriz.
@Override
public void onFastScrollStart() {
hideActionButton();
}
@Override
public void onFastScrollStop() {
showActionButton();
}
private void hideActionButton(){
ViewCompat.animate(actionButton).rotation(360).translationY(-100).alpha(0).setDuration(600).start();
}
private void showActionButton(){
ViewCompat.animate(actionButton).rotation(0).translationY(0).alpha(1).setDuration(600).start();
}
Bu şekilde fastScroll olayı başladığında FloatingActionButton'ı kaybediyoruz, bittiğinde geri getiriyoruz.
Yukarıda sadece onCreate metodunu ve global değerleri yazdım. Orada PageChangeListener
dikkatini çekmiştir eminim. Bununla da sayfa değiştiğinde gerekli tepkileri vereceğiz. Bildiğin gibi ana activity'ye bir ViewPager
koyduk ve içine iki tane fragment. Yani iki tane sayfamız var. ViewPager bu sayfalar arası geçişleri takip edebilmemiz için bize yine bir interface sağlıyor. Ve yine bildiğin gibi sayfların bir index değerleri var ve bunlar sıfırdan başlıyor. Biz ilk sayfayı ContactsFragment yaptık. Yani index değeri 0
sıfır. Arama kayıtlarının ise 1
bir oluyor tabiki. Bu olayı dinlemek için onCreate metodunda bir satır var.
viewPager.addOnPageChangeListener(this);
Bu satırdan dolayı artık gerekli olan interface'i uygulamamız gerek. Bu nterface bize üç metot tanımlattırıyor.
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
@Override
public void onPageScrollStateChanged(int state) {}
@Override
public void onPageSelected(int position){}
Bu üçünü de tanımlamak zorundayız ama sadece bir tanesine kod yazacağız, onPageSelected. Yani sayfa seçildiğinde. İster kaydırarak seçelim, ister tab'lara dokunarak seçelim bu olayı alacağız. Bu olayı nerede alıyoruz? MainActivity'de. Çünkü viewPager MainActivity'de. Bu olayı hem burada işleyeceğiz hem de başka dinleyicilere duyuracağız. Diğer dinleyiciler sayfalar. Hem kişiler sayfası hem arama kayıtları sayfası bu olaya ihtiyaç duyuyor. Neden? Açılış sayfamız ContactsFragment değil mi? Ve her sayfa kendi izinlerini halledecek dedik. Bu durumda ilk açılışta ContactsFragment rehber izinlerini soracak hemen. Ama aynı zamanda arama kayıtları için de izin gerekli. Ancak bunu ilk açılışta hemen sormayacağız. Bu arada şunu da söyleyeyim, biz sayfaları viewPager'a koyduğumuzda ilk açılışta ikisi birden örnekleniyor. Hatta örnekleyip öyle koyuyoruz. İlk açılışta ContactsFragment kendi izinlerini soracak, arama kayıtları ise sayfanın kendisine gelmesini bekleyecek. Yani sayfayı değiştirene kadar arama kaydı izni sormayacak. Demekki sayfanın hangi sayfa olduğunu ve sayfa değiştiğinde bu sayfanın hani sayfa olduğunu bilmeye ihtiyacı var. .
Kişiler sayfasının da bu bilgilere ihtiyacı var. Neden? Diyelimki ilk açılışta rehber izinleri sorulduğunda red cevabı aldık. Bu durumda sayfa değişip tekrar kişiler sayfasına geldiğinde izinler tekrar sorulacak. Bu, kullanıcı için bir kolaylık. Aslında bir kolaylık daha sağlayabiliriz. Uygulamamızın bir menüsü var ve oraya izinlerin durumunu gösterecek bir seçenek ekleyebiliriz. Ve izinler reddedilmişse tekrar sorarız. Şimdilik sadece sayfa değişikliğinde bakalım bu duruma. Bu, her iki sayfa için geçerli. Ayrıca birazdan bir sorundan bahsedeceğiz ve bu menüye birşey daha ekleyeceğiz. Neyse. Şimdi MainActivity'de arama kayıtları sayfasına geçildiğinde FloatingActionButton'ı nasıl kaybedeceğiz ona bakalım.
@Override
public void onPageSelected(int position) {
log.w("onPageSelected(%d)", position);
if(position == 1) hideActionButton();
if(position == 0) showActionButton();
}
Biz daha önce FloatingActionButton'ı kaybetmiştik değil m? Aynı metotla yine kaybedip geri getiriyoruz. Sayfa index'i 1
ise bunun anlamı arama kayıtları sayfasına geçilmiş demektir. Bu durumda hideActionButton
metodunu kullanıyoruz. Bu sıfır ve bir'i bir değişmeze atayabiliriz.
private static final int CONTACTS_PAGE = 0;
private static final int CALLLOG_PAGE = 1;
Böylece neyin ne olduğunu daha anlamlı hale getirmiş oluruz.
Sayfa değişikliğine gereken tepkiyi verdik. Ama bu MainActivity sadece. FloatingActionButton burada olduğu için kafamıza göre getir götür yapabiliriz. Ancak bu olayı diğer sayfalara da haber vermeliyiz.
@Override
public void onPageSelected(int position) {
log.w("onPageSelected(%d)", position);
if(position == CALLLOG_PAGE) hideActionButton();
if(position == CONTACTS_PAGE) showActionButton();
if(pageChangeListeners == null) return;
notifyPageListeners(position);
}
pageChangeListeners
aslında hiç bir zaman null
olmayacak. Çünkü onCreate
metodunda bunu tanımlıyoruz.
contactsFragment = new ContactsFragment();
callLogFragment = new CallLogFragment();
pageChangeListeners = new PageChangeListener[]{callLogFragment, contactsFragment};
Ancak içine koyduğumuz nesneler bir sebepten null
gelebilir.
private void notifyPageListeners(final int index){
for (PageChangeListener pageChangeListener : pageChangeListeners) {
if(pageChangeListener != null) pageChangeListener.onPageChange(index);
}
}
Bu olay için bir addListener
metodu kullanmadık çünkü sayfalar zaten elimizde, direk elimizle ekledik. Fakat sayfalarımızın bu olayla etileşim kurabilmesi için tabiki bir interface tanımladık.
public interface PageChangeListener {
void onPageChange(final int pageIndex);
}
Ve ContactsFragment sayfasının bu olaya tepkisi çok basit.
@Override
public void onPageChange(int pageIndex) {
if (THIS_PAGE == pageIndex) {
if (contacts == null) start();
}
}
THIS_PAGE
değişmezi tabiki 0
sıfır. Yani kişiler sayfası. Yani içinde bulunduğumuz sayfa. Eğer öyle ise contacts
değişkenine bakıyoruz. Bu değişken kişilerin bir listesi değil mi? Eğer bu değişken null
ise ya izin reddedilmiştir ya da sayfa ilk kez açılıyordur. Bu durumda start
metodumuzu ateşliyoruz. Neydi bu metot hatırlayalım.
private void start() {
if (getContext() == null) return;
if (EasyPermissions.hasPermissions(getContext(), CONTACTS_PERMISSIONS)) {
onPermissionsGranted();
return;
}
requestPermissions(CONTACTS_PERMISSIONS, RC_CONTACTS);
/*EasyPermissions.requestPermissions(
this,
getString(R.string.contacts_permission_rationale),
RC_CONTACTS,
CONTACTS_PERMISSIONS);*/
}
Bu metot herşeyin başladığı yer. İzinler var ise kişileri alıp getirecek hamleyi yapıyor. Yok ise izni soracak. İzin verilirse de yine gidip kişileri alıyoruz. İzni sorarken küçük bir değişiklik yaptım. EasyPermissions ile sorduğumuzda bir rationale belirtmek zorundayız kullanıcı reddederse diye. Eğer izin bir kez reddedilirse bu rationale gösterilecek kullanıcıya. Yani küçük bir açıklama yazısı. Ama ben böyle bir açıklama yapmak istemiyorum. Bu yüzden fragment sınıfının kendi requestPermissions
metodunu kullandım. Ama izinlerden geri dönüşler yine EasyPermissions ile olacak sıkıntı yok. Bu arada projemizin build.gradle dosyasını ve manifest dosyasını vermeyi unutmuşum. Sen de hiç hatırlatmıyorsun.
build.gradle
apply plugin: 'com.android.application'
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
compileSdkVersion 28
defaultConfig {
applicationId "com.tr.hsyn.telefonrehberi"
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = "true"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'com.android.support:appcompat-v7:28.0.0-alpha1'
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
implementation 'com.android.support:recyclerview-v7:28.0.0-alpha1'
implementation 'com.android.support:design:28.0.0-alpha1'
implementation 'com.android.support:support-v4:28.0.0-alpha1'
implementation 'de.hdodenhof:circleimageview:2.2.0'
implementation 'com.simplecityapps:recyclerview-fastscroll:1.0.17'
implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.10'
implementation 'pub.devrel:easypermissions:1.2.0'
implementation 'org.jetbrains:annotations:15.0'
implementation 'org.jetbrains:annotations:15.0'
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.github.Binary-Finery:Bungee:master-SNAPSHOT'
}
repositories {
google()
mavenCentral()
jcenter()
maven {
url 'http://dl.bintray.com/amulyakhare/maven'
}
maven {
url "https://maven.google.com"
}
maven { url 'https://jitpack.io' }
}
gradle dosyasına ekleyeceğimiz bir kaç mevzu daha var ama şimdi değil.
manifest.xml
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tr.hsyn.telefonrehberi">
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".activities.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
manifest dosyamız da bundan çok daha şenlikli olacak ilerde. Bu telefon rehberi için ilginç fikirler var aklımda.
Son olarak sorunlu bir konudan bahsetmek istiyorum. O da şu ki, telefon rehberinde kendine özel hesap açan bazı uygulamalardan dolayı kişiler rehberde tekrarlı görünebilir. Bunu ayarlardan, hesap ayarlarına giderek, bu tekrara sebep olduğunu düşündüğümüz hesabı kaldırarak çözebiliriz. Mesela ben birçok kez whatsapp hesabını kaldırdım. Whatsapp'ın değişik versiyonları da var, mesela gbwhatsapp, whatsappplus falan filan. Bunlar bazen böyle bir soruna sebep olabiliyor. Buna başka uygulamalar da sebep olabilir. Bu sorunu ayarlardan çözebiliriz ama buna ek olarak uygulamamıza bir hesap seçme imkanı da sunmamız gerek. Ve sadece seçilen hesaba ait kayıtları gösteririz. Bu imkanı menü aracılığı ile sağlayacağız. Fakat bu olay uygulamanın bir parça gidişatını değiştirecek. Daha doğrusu ContactsFragment'ı değiştirecek. Bu sınıfın en tepesine bir değişken tanımlayarak başlıyoruz.
private Account selectedAccount;
Bu hesap seçilen hesap varsa onu gösterecek. Bir Account
nesnesinin name ve type bilgileri vardır. Biz bir hesap seçilirse bu iki bilgiyi kaydedeceğiz ve açılışta bunu kontrol ederek başlayacağız ve ona göre kişileri yükleyeceğiz. Kullanıcı hesap seçimini menüden başlatacak.
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_select_account"
android:title="@string/menu_select_account"
app:showAsAction="never" />
</menu>
Bu menü öğesi seçildiğinde çalıştıracağımız metot :
public void selectAccount() {
Intent intent = AccountPicker.newChooseAccountIntent(
selectedAccount,
null,
null/*new String[]{"com.google", "WhatsApp"}*/,
false,
null,
null,
null,
null);
startActivityForResult(intent, SELECT_ACCOUNT);
}
AccountPicker için build.gradle dosyasına bir ekleme yapıyoruz.
implementation 'com.google.android.gms:play-services-auth:15.0.1'
Bunu başka mevzular için de kullanacağız. Mesela bir google hesabı ile giriş yapmak için.
Kullanıcıya bir hesap seçimi yaptırabilmemiz için tabiki telefondaki hesapları almamız gerek değil mi? Bu işlem de bir izne tâbi.
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
Mâzinin bir yerinde tüm hesapları alan bir fonksiyon yazmıştık hatırlarsan.
public static Account[] getAccounts(final Context context){
if(context == null) return null;
AccountManager manager = (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
if(manager == null) return null;
return manager.getAccounts();
}
Bunu birazdan kullanacağız.
Bir hesap seçileceği için bize bu hesap üzenden kişileri verecek bir metot gerekli. Contacts sınıfımıza bunu ekliyoruz.
public static List<Contact> getContacts(final Context context, final String accountName) {
if (context == null) return null;
if(accountName == null || accountName.isEmpty()) return getContacts(context);
final Cursor cursor = context.getContentResolver().query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
PHONE_COLUMNS,
PHONE_COLUMNS[4] + "=?",
new String[]{accountName},
PHONE_COLUMNS[3] + " asc"
);
if (cursor == null) return null;
final List<Contact> contacts = new ArrayList<>();
if (cursor.getCount() == 0) {
cursor.close();
return contacts;
}
final int idCol = cursor.getColumnIndex(PHONE_COLUMNS[0]);
final int nameCol = cursor.getColumnIndex(PHONE_COLUMNS[1]);
final int thumbNamilCol = cursor.getColumnIndex(PHONE_COLUMNS[2]);
while (cursor.moveToNext()) {
final String thumbNail = cursor.getString(thumbNamilCol);
final Contact contact = new Contact(
cursor.getString(idCol),
cursor.getString(nameCol),
thumbNail == null ? null : Uri.parse(thumbNail));
contacts.add(contact);
}
cursor.close();
return contacts;
}
Metodun başında, verilen hesabı kontrol ediyoruz.
if(accountName == null || accountName.isEmpty()) return getContacts(context);
Eğer accountName verilmemiş ise ilk yazdığımız metodu döndürüyoruz. Bu arada farketmediğine eminim ki, sütunlar için yeni bir dizi tanımladık. Aslında bu sınıfı komple bir görsek çok süper olacak.
Contacts.java
public class Contacts {
private Contacts() {}
private final static String[] CONTACT_COLUMNS = {
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.PHOTO_THUMBNAIL_URI,
ContactsContract.Contacts.SORT_KEY_PRIMARY
};
private final static String[] PHONE_COLUMNS = {
ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.PHOTO_THUMBNAIL_URI,
ContactsContract.CommonDataKinds.Phone.SORT_KEY_PRIMARY,
ContactsContract.RawContacts.ACCOUNT_NAME
};
@Nullable
public static List<Contact> getContacts(final Context context){
if(context == null) return null;
final Cursor cursor = context.getContentResolver().query(
ContactsContract.Contacts.CONTENT_URI,
CONTACT_COLUMNS,
null,
null,
CONTACT_COLUMNS[3] + " asc"
);
if(cursor == null) return null;
final List<Contact> contacts = new ArrayList<>();
if(cursor.getCount() == 0) return contacts;
final int idColumn = cursor.getColumnIndex(CONTACT_COLUMNS[0]);
final int nameColumn = cursor.getColumnIndex(CONTACT_COLUMNS[1]);
final int photoColumn = cursor.getColumnIndex(CONTACT_COLUMNS[2]);
while (cursor.moveToNext()) {
String photo = cursor.getString(photoColumn);
contacts.add(
new Contact(
cursor.getString(idColumn),
cursor.getString(nameColumn),
photo != null ? Uri.parse(photo) : null
));
}
cursor.close();
return contacts;
}
public static List<Contact> getContacts(final Context context, final String accountName) {
if (context == null) return null;
if(accountName == null || accountName.isEmpty()) return getContacts(context);
final Cursor cursor = context.getContentResolver().query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
PHONE_COLUMNS,
PHONE_COLUMNS[4] + "=?",
new String[]{accountName},
PHONE_COLUMNS[3] + " asc"
);
if (cursor == null) return null;
final List<Contact> contacts = new ArrayList<>();
if (cursor.getCount() == 0) {
cursor.close();
return contacts;
}
final int idCol = cursor.getColumnIndex(PHONE_COLUMNS[0]);
final int nameCol = cursor.getColumnIndex(PHONE_COLUMNS[1]);
final int thumbNamilCol = cursor.getColumnIndex(PHONE_COLUMNS[2]);
while (cursor.moveToNext()) {
final String thumbNail = cursor.getString(thumbNamilCol);
final Contact contact = new Contact(
cursor.getString(idCol),
cursor.getString(nameCol),
thumbNail == null ? null : Uri.parse(thumbNail));
contacts.add(contact);
}
cursor.close();
return contacts;
}
public static Account[] getAccounts(final Context context){
if(context == null) return null;
AccountManager manager = (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
if(manager == null) return null;
return manager.getAccounts();
}
}
Artık tanımladığımız getContacts
metodunun ikinci versiyonunu kullanacağız. Seçilmiş bir hesap yoksa kendisi ilk versiyonu çağıracak zaten. Biz bu metodu nerede kullanmıştık?
ContactsFragment.java
private List<Contact> getContacts() {
return Contacts.getContacts(getContext(), selectedAccount == null ? "" : selectedAccount.name);
}
Şimdi hesap seçme olayına geri dönelim.
public void selectAccount() {
Intent intent = AccountPicker.newChooseAccountIntent(
selectedAccount,
null,
null/*new String[]{"com.google", "WhatsApp"}*/,
false,
null,
null,
null,
null);
startActivityForResult(intent, SELECT_ACCOUNT);
}
Kullandığımız intent biraz karışık görünüyor. İlk parametre selectedAccount. Yani sınıfın en tepesinde tanımladığımız değişken. Bu değişken null
değil ise, hesap seçme ekranı açıldığında bu hesap seçili gözükecek. Çünkü null
değil ise bir hesap seçilmiş demektir değil mi? Eğer null
ise herhangi birşey seçili olmayacak. Yorum satırı olan üçüncü parametre, hesap seçme ekranında hangi tür hesapların görüneceğini belirtebilmemiz için. Eğer null
verirsek tüm hesaplar görünecek.
Bu seçimin sonucu onActivityResult metoduna geliyor. Bu metodu fragment içinde override edip, yeni bir kişi eklendiğinde rehberi yeniden yüklemek için kullanmıştık hatırlarsan. Şimdi burada hesap seçimi olayını da ele alacağız.
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case ADD_CONTACT:
if (resultCode == Activity.RESULT_OK) {
start();
}
break;
case SELECT_ACCOUNT:
if (resultCode == Activity.RESULT_OK) {
onAccountSelected(data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME));
}
break;
}
}
private void onAccountSelected(@NotNull final String accountName) {
final Account[] accounts;
if (selectedAccount != null && accountName.equals(selectedAccount.name) || (accounts = Contacts.getAccounts(getContext())) == null)
return;
for (Account account : accounts)
if (account.name.equals(accountName)) {
setSelectedAccount(account);
return;
}
}
public void setSelectedAccount(@NotNull final Account selectedAccount) {
if (getContext() == null) return;
SharedPreferences pref = getContext().getSharedPreferences(PREF_MAIN, Context.MODE_PRIVATE);
pref.edit()
.putString(SELECTED_ACCOUNT_NAME, selectedAccount.name)
.putString(SELECTED_ACCOUNT_TYPE, selectedAccount.type)
.apply();
onAccountChanged(selectedAccount);
}
private void onAccountChanged(@NotNull final Account selectedAccount) {
this.selectedAccount = selectedAccount;
start();
}
Kullandığımız değişmezler ise şöyle :
private static final int SELECT_ACCOUNT = 16;
private final String PREF_MAIN = "pref_main";
private final String SELECTED_ACCOUNT_NAME = "selected_account_name";
private final String SELECTED_ACCOUNT_TYPE = "selected_account_type";
Bir hesap seçildiğinde bunu kaydediyoruz. Ancak uygulama açıldığında da bunu kontrol etmemiz gerek değil mi?
ContactsFragment.java
@Override
public View onCreateView(@NotNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_contact_list, container, false);
String selectedAccountName = container.getContext().getSharedPreferences(PREF_MAIN, Context.MODE_PRIVATE).getString(SELECTED_ACCOUNT_NAME, "");
if (!selectedAccountName.isEmpty()) {
selectedAccount = new Account(selectedAccountName, container.getContext().getSharedPreferences(PREF_MAIN, Context.MODE_PRIVATE).getString(SELECTED_ACCOUNT_TYPE, ""));
}
recyclerView = view.findViewById(R.id.recyclerView);
LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
recyclerView.setLayoutManager(layoutManager);
progressBar = view.findViewById(R.id.progressBar);
setHasOptionsMenu(true);
Run.run(this::start, 60);
recyclerView.setStateChangeListener(new OnFastScrollStateChangeListener() {
@Override
public void onFastScrollStart() {
notifyFastScrollListeners(true);
}
@Override
public void onFastScrollStop() {
notifyFastScrollListeners(false);
}
});
return view;
}

Ancak bu hesap seçimini reset edecek bir seçenek de sunmamız gerek. Bunu da sana bırakıyorum.
Sanırım şimdilik konuyu bitirdik.