15 Temmuz 2018 Pazar

Android Contacts

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 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.

Hiç yorum yok:

Yorum Gönder