/* * Copyright 2011 Adi Sayoga. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.adisayoga.earthquake.services; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; import android.app.IntentService; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.location.Location; import android.location.LocationListener; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.util.Log; import android.widget.Toast; import com.adisayoga.earthquake.R; import com.adisayoga.earthquake.dto.EarthquakeDTO; import com.adisayoga.earthquake.dto.LocationType; import com.adisayoga.earthquake.models.ContactModel; import com.adisayoga.earthquake.models.EarthquakeModel; import com.adisayoga.earthquake.models.UsgsSource; import com.adisayoga.earthquake.providers.EarthquakeColumns; import com.adisayoga.earthquake.providers.EarthquakeProvider; import com.adisayoga.earthquake.receivers.EarthquakeReceiver; import com.adisayoga.earthquake.utils.BaseLocationListener; import com.adisayoga.earthquake.utils.LocationFinder; import com.adisayoga.earthquake.wrapper.EarthquakeFacebook; import com.adisayoga.earthquake.wrapper.EarthquakeMail; import com.adisayoga.earthquake.wrapper.EarthquakeNotification; import com.adisayoga.earthquake.wrapper.EarthquakeSms; import com.adisayoga.earthquake.wrapper.EarthquakeTemplate; import com.adisayoga.earthquake.wrapper.EarthquakeTwitter; import com.adisayoga.earthquake.wrapper.Prefs; /** * Service untuk mengecek data gempa baru, dan juga untuk notifikasi, share * ke jejaring sosial (facebook dan twitter), sms, dan email. * * @author Adi Sayoga */ public class EarthquakeService extends IntentService { private static final String TAG = "EarthquakeService"; private static final String NAME = "EarthquakeService"; private static Location location; private Prefs prefs; private EarthquakeFacebook facebook; private EarthquakeTwitter twitter; private EarthquakeMail mail; private LocationFinder locationFinder; private final Handler handler = new Handler(); public EarthquakeService() { super(NAME); } @Override public void onCreate() { super.onCreate(); prefs = Prefs.getInstance(this); facebook = new EarthquakeFacebook(this); twitter = new EarthquakeTwitter(this); mail = new EarthquakeMail(this); // Menentukan lokasi kita saat ini locationFinder = new LocationFinder(this); location = getLocation(); } /** * Mendapatkan lokasi (deteksi atau manual) sesuai dengan preference. * Jika dapat mendeteksi lokasi akan digunakan lokasi terakhir didapat, * atau jika tidak lokasi manual yang digunakan. * * @return Lokasi */ private Location getLocation() { locationFinder.setChangedLocationListener(locationListener); Location location; if (prefs.isDetectLocation()) { // Mendapatkan lokasi melalui GPS akan memerlukan waktu, jadi disini // kita akan mendapatkan lokasi user terakhir. location = locationFinder.getLastLocation(LocationFinder.MAX_DISTANCE, System.currentTimeMillis() + prefs.getInterval()); } else { location = prefs.getManualLocation(); } return location; } @Override protected void onHandleIntent(Intent intent) { // Hapus data lama deleteOldQuakes(prefs.getMaxAge()); try { // Mendapatkan data dari USGS long lastUpdate = prefs.getLastUpdate(); float minMagnitude = prefs.getMinMagnitude(); SimpleDateFormat sdf = new SimpleDateFormat("MM-dd HH:mm:ss"); Log.i(TAG, "Merefresh data... Last update=" + sdf.format(lastUpdate)); List<EarthquakeDTO> quakes = UsgsSource.read(lastUpdate, minMagnitude); // Kita sudah selesai mendapatkan data, simpan terakhir kali diupdate prefs.setLastUpdate(System.currentTimeMillis()); if (quakes != null && quakes.size() > 0) { // Terdapat data pada server, filter data ini sehingga data yang // didapat merupakan data yang benar-benar baru Log.d(TAG, "Data pada server: " + quakes.size() + " items"); quakes = getNewQuakes(quakes); } if (quakes != null && quakes.size() > 0) { // Terdapat data, simpan ke provider, beritahukan ada gempa baru // (jika sesuai dengan minimal magnitudo pengaturan), dan kirim // broadcast terdapat data baru Log.d(TAG, "Terdapat data baru: " + quakes.size() + " items"); addNewQuakes(quakes); notifyNewQuake(quakes); sendBroadcast(new Intent(EarthquakeReceiver.NEW_QUAKE_FOUND)); } else { // Tidak ada data baru, kirim broadcast tidak ada data baru Log.d(TAG, "Tidak ada data yang perlu di-update"); sendBroadcast(new Intent(EarthquakeReceiver.NO_NEW_QUAKE)); } } catch (IOException e) { // Terdapat error, kirim broadcast jaringan error Log.w(TAG, "Gagal mendapatkan data dari server"); sendBroadcast(new Intent(EarthquakeReceiver.NETWORK_ERROR)); } } /** * Menghapus data gempa yang lebih lama dari age yang ditentukan. * * @param age Umur dalam milisecond * @return Jumlah data yang dihapus */ private int deleteOldQuakes(long age) { EarthquakeModel table = new EarthquakeModel(this); return table.deleteQuakes(age); } /** * Mendapatkan data yang tidak ada pada provider. * * @param quakes Data gempa * @return Data baru */ private List<EarthquakeDTO> getNewQuakes(List<EarthquakeDTO> quakes) { List<EarthquakeDTO> newQuakes = new ArrayList<EarthquakeDTO>(); ContentResolver resolver = getContentResolver(); String selection = EarthquakeColumns.DATE + " = ?"; for (EarthquakeDTO quake : quakes) { String[] selectionArgs = new String[] { Long.toString(quake.time) }; Cursor cursor = resolver.query(EarthquakeProvider.CONTENT_URI, null, selection, selectionArgs, null); if (cursor.getCount() == 0) newQuakes.add(quake); cursor.close(); } return newQuakes; } /** * Menambahkan data gempa baru ke provider. * * @param quakes List data gempa * @return List gempa yang ditambahkan dengan id tabelnya */ private List<EarthquakeDTO> addNewQuakes(List<EarthquakeDTO> quakes) { Log.d(TAG, "Menyimpan data... "); ContentResolver resolver = getContentResolver(); for (EarthquakeDTO quake : quakes) { // Simpan ke provider ContentValues values = new ContentValues(); values.put(EarthquakeColumns.SRC, quake.source); values.put(EarthquakeColumns.EQID, quake.eqid); values.put(EarthquakeColumns.VERSION, quake.version); values.put(EarthquakeColumns.DATE, quake.time); values.put(EarthquakeColumns.LATITUDE, quake.latitude); values.put(EarthquakeColumns.LONGITUDE, quake.longitude); values.put(EarthquakeColumns.MAGNITUDE, quake.magnitude); values.put(EarthquakeColumns.DEPTH, quake.depth); values.put(EarthquakeColumns.NST, quake.nst); values.put(EarthquakeColumns.REGION, quake.region); Uri uri = resolver.insert(EarthquakeProvider.CONTENT_URI, values); // Mendapatkan id dari data yang baru saja diinsert long segment = Long.parseLong(uri.getPathSegments().get(1)); quake.id = segment; } return quakes; } /** * Memberitahukan bahwa terdapat gempa baru (notifikasi, kirim SMS, share ke * Facebook). * <p> * Disini dilakukan filter yang sesuai dengan minimal magnitudo pengaturan untuk * masing-masing pemberitahuan. * * @param quakes List gempa */ private void notifyNewQuake(List<EarthquakeDTO> quakes) { if (quakes == null || quakes.size() == 0) return; // Load preference boolean isNotify = prefs.isNotifySend(); boolean isSmsSend = prefs.isSmsSend(); boolean isFacebookSend = prefs.isFacebookSend(); boolean isTwitterSend = prefs.isTwitterSend(); boolean isMailSend = prefs.isMailSend(); float notifyMinMagReg = prefs.getNotifyMinMagnitude(LocationType.REGIONAL); float notifyMinMagGlobal = prefs.getNotifyMinMagnitude(LocationType.GLOBAL); float facebookMinMagReg = prefs.getFacebookMinMagnitude(LocationType.REGIONAL); float facebookMinMagGlobal = prefs.getFacebookMinMagnitude(LocationType.GLOBAL); float twitterMinMagReg = prefs.getTwitterMinMagnitude(LocationType.REGIONAL); float twitterMinMagGlobal = prefs.getTwitterMinMagnitude(LocationType.GLOBAL); float mailMinMagReg = prefs.getMailMinMagnitude(LocationType.REGIONAL); float mailMinMagGlobal = prefs.getMailMinMagnitude(LocationType.GLOBAL); float smsMinMagReg = prefs.getSmsMinMagnitude(LocationType.REGIONAL); float smsMinMagGlobal = prefs.getSmsMinMagnitude(LocationType.GLOBAL); float prefMinMag = prefs.getMinMagnitude(); int prefRange = prefs.getRange(); // Variable untuk menentukan data gempa yang paling besar dan diprioritaskan // untuk lokasi regional float lastMagReg = 0; float lastMagGlobal = 0; int quakeCount = 0; // Variable untuk menyimpan data EarthquakeDTO quakeNotify = null; List<EarthquakeDTO> quakesFacebook = new ArrayList<EarthquakeDTO>(); List<EarthquakeDTO> quakesTwitter = new ArrayList<EarthquakeDTO>(); List<EarthquakeDTO> quakesMail = new ArrayList<EarthquakeDTO>(); List<EarthquakeDTO> quakesSms = new ArrayList<EarthquakeDTO>(); for (EarthquakeDTO quake : quakes) { float magnitude = quake.magnitude; if (magnitude < prefMinMag) continue; // Jarak dari masing-masing gempa berada pada range regional atau tidak boolean inRange = false; if (location != null) { float distance = quake.getLocation().distanceTo(location); inRange = distance <= prefRange; } // Notifikasi status bar, diprioritaskan untuk regional if (isNotify) { if (inRange && notifyMinMagReg <= magnitude) { quakeCount++; if (lastMagReg < magnitude) { quakeNotify = quake; lastMagReg = magnitude; } } else if (notifyMinMagGlobal <= magnitude) { quakeCount++; if (lastMagGlobal < magnitude) { // Jika belum ada notifikasi regional maka ini yang dipakai, // jika tidak maka biarkan yang regional if (lastMagReg == 0) quakeNotify = quake; lastMagGlobal = magnitude; } } } // Share ke Facebook if (isFacebookSend) addQuakeNotify(quakesFacebook, quake, inRange, magnitude, facebookMinMagReg, facebookMinMagGlobal); // Share ke Twitter if (isTwitterSend) addQuakeNotify(quakesTwitter, quake, inRange, magnitude, twitterMinMagReg, twitterMinMagGlobal); // Kirim email if (isMailSend) addQuakeNotify(quakesMail, quake, inRange, magnitude, mailMinMagReg, mailMinMagGlobal); // SMS if (isSmsSend) addQuakeNotify(quakesSms, quake, inRange, magnitude, smsMinMagReg, smsMinMagGlobal); } // Tampilkan notifikasi, dan/atau share if (quakeCount > 0) sendNotification(quakeNotify, quakeCount); if (quakesFacebook != null && quakesFacebook.size() > 0) shareToFacebook(quakesFacebook); if (quakesTwitter != null && quakesTwitter.size() > 0) shareToTwitter(quakesTwitter); if (quakesMail != null && quakesMail.size() > 0) sendMail(quakesMail); if (quakesSms != null && quakesSms.size() > 0) sendSms(quakesSms); } /** * Tambahkan data gempa dengan filter minimal magnitudo regional dan global. * * @param toAdd Data gempa yang ditambahkan * @param quake Data gempa sumber * @param inRange True untuk dalam jarak regional, false untuk global * @param magnitude Magnitudo gempa * @param minMagReg Minimal magnitudo regional * @param minMagGlobal Minimal magnitudo global */ private void addQuakeNotify(List<EarthquakeDTO> toAdd, EarthquakeDTO quake, boolean inRange, float magnitude, float minMagReg, float minMagGlobal) { if ((inRange && minMagReg <= magnitude) || (minMagGlobal <= magnitude)) { toAdd.add(quake); } } /** * Kirim notifikasi. * * @param quake Data gempa * @param quakeCount Jumlah gempa */ private void sendNotification(EarthquakeDTO quake, int quakeCount) { Log.d(TAG, "Mengirim notifikasi..."); boolean isFlash = prefs.isNotifyFlash(); boolean isAlert = prefs.isNotifyAlert(); Uri alertSound = prefs.getNotifyAlertSound(); boolean isVibrate = prefs.isNotifyVibrate(); EarthquakeNotification notifier = new EarthquakeNotification( this, quake, quakeCount, isAlert, alertSound, isFlash, isVibrate); notifier.alert(); } /** * Share ke Facebook. Dilakukan hanya jika sudah login. * <p> * Sebagai catatan, service berjalan pada background, sehingga kita tidak perlu * menampilkan dialog login, karena itu mungkin akan menjengkelkan user. * * @param quakes Data gempa */ private void shareToFacebook(List<EarthquakeDTO> quakes) { if (!facebook.isSessionValid()) return; Log.d(TAG, "Share ke Facebook..."); // Post setiap list quake boolean postSent = false; for (EarthquakeDTO quake : quakes) { Bundle params = facebook.genereateParams(quake, null, location); postSent |= facebook.postMessage(params); } showMessage((postSent) ? R.string.facebook_post_sent : R.string.facebook_post_fail); } /** * Share ke Twitter. Dilakukan hanya jika sudah login. * <p> * Sebagai catatan, service berjalan pada background, sehingga kita tidak perlu * menampilkan dialog login, karena itu mungkin akan menjengkelkan user. * <p> * Pesan hanya akan dikirim sekali saja, tidak dapat dikirim berulang-ulang * untuk setiap data gempa karena akan terdapat error update limit. * Twitte juga dibatasi 140 karakter, jadi pesan yang dikirim akan dipotong. * <p> * TODO: Ada cara lebih baik? * <p> * * @param quakes Data gempa */ private void shareToTwitter(final List<EarthquakeDTO> quakes) { Log.d(TAG, "Share ke Twitter..."); twitter.login(null, new EarthquakeTwitter.AuthListener() { @Override public void onAuthComplete() { String message = ""; for (EarthquakeDTO quake : quakes) { if (message != "") message += ", "; message += twitter.getPostMessage(quake, location); } // Message maksimal 140 karakter message = message.substring(0, 137) + "..."; boolean postSent = twitter.postMessage(message); showMessage((postSent) ? R.string.twitter_post_sent : R.string.twitter_post_fail); } @Override public void onAuthFail() { showMessage(R.string.twitter_post_fail); } }); } /** * Kirim pesan ke email. * * @param quakes Data gempa */ private void sendMail(List<EarthquakeDTO> quakes) { try { Log.d(TAG, "Mengirim Email..."); Context context = this; mail.setFrom(prefs.getMailUsername()); ContactModel table = new ContactModel(this); String[] mails = table.getMails(); mail.setTo(mails); mail.setSubject(context.getString(R.string.app_name)); String message = EarthquakeTemplate.getInstance(context).getMessage( prefs.getMailTemplate(context), prefs.getMailTemplateDetail(context), quakes, location); mail.setBody(message); boolean mailSent = mail.send(); showMessage((mailSent) ? R.string.mail_sent : R.string.mail_fail); } catch (Exception e) { showMessage(R.string.mail_fail); Log.e(TAG, e.getMessage(), e); } } /** * Kirim pesan SMS. * * @param quakes Data gempa */ private void sendSms(List<EarthquakeDTO> quakes) { Log.d(TAG, "Mengirim sms..."); Context context = this; String message = EarthquakeTemplate.getInstance(context).getMessage( prefs.getSmsTemplate(context), prefs.getSmsTemplateDetail(context), quakes, location); ContactModel table = new ContactModel(this); String[] phones = table.getPhones(); EarthquakeSms sms = new EarthquakeSms(this); List<String> phonesSent = sms.sendTextMessage(phones, message, EarthquakeSms.SPLIT_SMS_MESSAGE); boolean smsSent = phonesSent != null && phonesSent.size() > 0; showMessage((smsSent) ? R.string.sms_sent : R.string.sms_fail); } /** * Tampilkan pesan menggunakan Toast. * * @param resId Resource id */ private void showMessage(final int resId) { // Tampilkan pesan pada UI thread. // Toast tidak dapat berjalan pada bacground thread handler.post(new Runnable() { @Override public void run() { Toast.makeText(EarthquakeService.this, resId, Toast.LENGTH_SHORT); } }); } private final LocationListener locationListener = new BaseLocationListener() { @Override public void onLocationChanged(Location newLocation) { location = newLocation; } }; }