Java tutorial
/* * MIT License * * Copyright (c) 2016. Dmytro Karataiev * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.adkdevelopment.earthquakesurvival.data.syncadapter; import android.accounts.Account; import android.accounts.AccountManager; import android.app.Notification; import android.app.PendingIntent; import android.content.AbstractThreadedSyncAdapter; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SyncRequest; import android.content.SyncResult; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; import android.text.Html; import android.text.SpannedString; import android.text.format.DateUtils; import android.util.Log; import com.adkdevelopment.earthquakesurvival.App; import com.adkdevelopment.earthquakesurvival.R; import com.adkdevelopment.earthquakesurvival.data.objects.earthquake.EarthquakeObject; import com.adkdevelopment.earthquakesurvival.data.objects.earthquake.Feature; import com.adkdevelopment.earthquakesurvival.data.objects.info.CountEarthquakes; import com.adkdevelopment.earthquakesurvival.data.objects.news.Channel; import com.adkdevelopment.earthquakesurvival.data.objects.news.Item; import com.adkdevelopment.earthquakesurvival.data.objects.news.Rss; import com.adkdevelopment.earthquakesurvival.data.provider.count.CountColumns; import com.adkdevelopment.earthquakesurvival.data.provider.earthquake.EarthquakeColumns; import com.adkdevelopment.earthquakesurvival.data.provider.news.NewsColumns; import com.adkdevelopment.earthquakesurvival.ui.DetailActivity; import com.adkdevelopment.earthquakesurvival.utils.LocationUtils; import com.adkdevelopment.earthquakesurvival.utils.Utilities; import com.google.android.gms.maps.model.LatLng; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.Vector; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; /** * SyncAdapter, which performs all network-related work, * sends notifications each day. * Created by karataev on 4/1/16. */ public class SyncAdapter extends AbstractThreadedSyncAdapter { private static final String TAG = SyncAdapter.class.getSimpleName(); // Broadcast message to the widget public static final String ACTION_DATA_UPDATE = "com.adkdevelopment.earthquakesurvival.ACTION_DATA_UPDATED"; public SyncAdapter(Context context, boolean autoInitialize) { super(context, autoInitialize); } @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { Context context = getContext(); App.getApiManager().getEarthquakeService().getData().enqueue(new Callback<EarthquakeObject>() { @Override public void onResponse(Call<EarthquakeObject> call, Response<EarthquakeObject> response) { EarthquakeObject earthquake = response.body(); Vector<ContentValues> cVVector = new Vector<>(earthquake.getFeatures().size()); double currentBiggest = 0.0; ContentValues notifyValues = null; for (Feature each : earthquake.getFeatures()) { ContentValues earthquakeValues = new ContentValues(); earthquakeValues.put(EarthquakeColumns.PLACE, each.getProperties().getPlace()); earthquakeValues.put(EarthquakeColumns.ID_EARTH, each.getId()); earthquakeValues.put(EarthquakeColumns.MAG, each.getProperties().getMag()); earthquakeValues.put(EarthquakeColumns.TYPE, each.getProperties().getType()); earthquakeValues.put(EarthquakeColumns.ALERT, each.getProperties().getAlert()); earthquakeValues.put(EarthquakeColumns.TIME, each.getProperties().getTime()); earthquakeValues.put(EarthquakeColumns.URL, each.getProperties().getUrl()); earthquakeValues.put(EarthquakeColumns.DETAIL, each.getProperties().getDetail()); earthquakeValues.put(EarthquakeColumns.DEPTH, each.getGeometry().getCoordinates().get(2)); earthquakeValues.put(EarthquakeColumns.LONGITUDE, each.getGeometry().getCoordinates().get(0)); earthquakeValues.put(EarthquakeColumns.LATITUDE, each.getGeometry().getCoordinates().get(1)); LatLng latLng = new LatLng(each.getGeometry().getCoordinates().get(1), each.getGeometry().getCoordinates().get(0)); LatLng location = LocationUtils.getLocation(context); earthquakeValues.put(EarthquakeColumns.DISTANCE, LocationUtils.getDistance(latLng, location)); cVVector.add(earthquakeValues); if (each.getProperties().getMag() != null && each.getProperties().getMag() > currentBiggest) { currentBiggest = each.getProperties().getMag(); notifyValues = new ContentValues(earthquakeValues); notifyValues.put(EarthquakeColumns.PLACE, Utilities.formatEarthquakePlace(each.getProperties().getPlace())); } } int inserted = 0; // add to database ContentResolver resolver = context.getContentResolver(); if (cVVector.size() > 0) { ContentValues[] cvArray = new ContentValues[cVVector.size()]; cVVector.toArray(cvArray); inserted = resolver.bulkInsert(EarthquakeColumns.CONTENT_URI, cvArray); } // Set the date to day minus one to delete old data from the database Date date = new Date(); date.setTime(date.getTime() - DateUtils.DAY_IN_MILLIS); int deleted = resolver.delete(EarthquakeColumns.CONTENT_URI, EarthquakeColumns.TIME + " <= ?", new String[] { String.valueOf(date.getTime()) }); Log.v(TAG, "Service Complete. " + inserted + " Inserted, " + deleted + " deleted"); sendNotification(notifyValues); } @Override public void onFailure(Call<EarthquakeObject> call, Throwable t) { Log.e(TAG, "onFailure: " + t.toString()); } }); App.getNewsManager().getNewsService().getNews().enqueue(new Callback<Rss>() { @Override public void onResponse(Call<Rss> call, Response<Rss> response) { Channel news = response.body().getChannel(); Vector<ContentValues> cVVector = new Vector<>(news.getItem().size()); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.getDefault()); Date date = new Date(); for (Item each : news.getItem()) { ContentValues weatherValues = new ContentValues(); try { date = simpleDateFormat.parse(each.getPubDate()); } catch (ParseException e) { Log.e(TAG, "e:" + e); } weatherValues.put(NewsColumns.DATE, date.getTime()); weatherValues.put(NewsColumns.TITLE, each.getTitle()); weatherValues.put(NewsColumns.DESCRIPTION, Html.toHtml(new SpannedString(each.getDescription()))); weatherValues.put(NewsColumns.URL, each.getLink()); weatherValues.put(NewsColumns.GUID, each.getGuid().getContent()); cVVector.add(weatherValues); } int inserted = 0; // add to database ContentResolver resolver = getContext().getContentResolver(); if (cVVector.size() > 0) { // Student: call bulkInsert to add the weatherEntries to the database here ContentValues[] cvArray = new ContentValues[cVVector.size()]; cVVector.toArray(cvArray); inserted = resolver.bulkInsert(NewsColumns.CONTENT_URI, cvArray); } // Set the date to day minus two to delete old data from the database date = new Date(); date.setTime(date.getTime() - DateUtils.DAY_IN_MILLIS * 3); int deleted = resolver.delete(NewsColumns.CONTENT_URI, NewsColumns.DATE + " <= ?", new String[] { String.valueOf(date.getTime()) }); } @Override public void onFailure(Call<Rss> call, Throwable t) { Log.e(TAG, "onFailure: " + t.toString()); } }); // TODO: 4/22/16 possible refactoring //checking the last update and notify if it' the first of the day SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); String lastNotificationKey = context.getString(R.string.sharedprefs_key_last_countupdate); long lastSync = prefs.getLong(lastNotificationKey, DateUtils.DAY_IN_MILLIS); if (System.currentTimeMillis() - lastSync >= Utilities.getSyncIntervalPrefs(context) * DateUtils.SECOND_IN_MILLIS) { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US); Date date = new Date(System.currentTimeMillis()); String startTime[] = new String[] { simpleDateFormat.format(date.getTime() - DateUtils.YEAR_IN_MILLIS), simpleDateFormat.format(date.getTime() - DateUtils.DAY_IN_MILLIS * 30), simpleDateFormat.format(date.getTime() - DateUtils.WEEK_IN_MILLIS), simpleDateFormat.format(date.getTime() - DateUtils.DAY_IN_MILLIS) }; String endTime = simpleDateFormat.format(date); int iterator = 1; while (iterator < CountColumns.ALL_COLUMNS.length) { final int round = iterator; App.getApiManager().getEarthquakeService().getEarthquakeStats(startTime[round - 1], endTime) .enqueue(new Callback<CountEarthquakes>() { @Override public void onResponse(Call<CountEarthquakes> call, Response<CountEarthquakes> response) { ContentValues count = new ContentValues(); count.put(CountColumns.ALL_COLUMNS[round], response.body().getCount()); ContentResolver contentResolver = context.getContentResolver(); Cursor cursor = contentResolver.query(CountColumns.CONTENT_URI, null, null, null, null); if (cursor != null) { if (cursor.getCount() < 1) { long inserted = ContentUris .parseId(contentResolver.insert(CountColumns.CONTENT_URI, count)); //Log.d(TAG, "inserted:" + inserted); } else { int updated = contentResolver.update(CountColumns.CONTENT_URI, count, CountColumns._ID + " = ?", new String[] { "1" }); //Log.d(TAG, "updated: " + updated); } cursor.close(); } } @Override public void onFailure(Call<CountEarthquakes> call, Throwable t) { Log.e(TAG, "Error: " + t); } }); iterator++; } //refreshing last sync prefs.edit().putLong(lastNotificationKey, System.currentTimeMillis()).apply(); } // notify PagerActivity that data has been updated context.getContentResolver().notifyChange(EarthquakeColumns.CONTENT_URI, null, false); context.getContentResolver().notifyChange(NewsColumns.CONTENT_URI, null, false); context.getContentResolver().notifyChange(CountColumns.CONTENT_URI, null, false); updateWidgets(); } public static void initializeSyncAdapter(Context context) { getSyncAccount(context); } /** * Helper method to get the fake account to be used with SyncAdapter, or make a new one * if the fake account doesn't exist yet. If we make a new account, we call the * onAccountCreated method so we can initialize things. * * @param context The context used to access the account service * @return a fake account. */ private static Account getSyncAccount(Context context) { // Get an instance of the Android account manager AccountManager accountManager = (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE); // Create the account type and default account Account newAccount = new Account(context.getString(R.string.app_name), context.getString(R.string.sync_account_type)); // If the password doesn't exist, the account doesn't exist if (null == accountManager.getPassword(newAccount)) { /* * Add the account and account type, no password or user data * If successful, return the Account object, otherwise report an error. */ if (!accountManager.addAccountExplicitly(newAccount, "", null)) { return null; } /* * If you don't set android:syncable="true" in * in your <provider> element in the manifest, * then call ContentResolver.setIsSyncable(account, AUTHORITY, 1) * here. */ onAccountCreated(newAccount, context); } return newAccount; } private static void onAccountCreated(Account newAccount, Context context) { /* * Since we've created an account */ SyncAdapter.configurePeriodicSync(context, Utilities.getSyncIntervalPrefs(context), Utilities.getSyncIntervalPrefs(context) / 3); /* * Without calling setSyncAutomatically, our periodic sync will not be enabled. */ ContentResolver.setSyncAutomatically(newAccount, context.getString(R.string.sync_provider_authority), true); /* * Finally, let's do a sync to get things started */ syncImmediately(context); } /** * Helper method to schedule the sync adapter periodic execution */ public static void configurePeriodicSync(Context context, int syncInterval, int flexTime) { Account account = getSyncAccount(context); String authority = context.getString(R.string.sync_provider_authority); if (syncInterval == -1) { ContentResolver.setSyncAutomatically(account, context.getString(R.string.sync_provider_authority), false); } else { ContentResolver.setSyncAutomatically(account, context.getString(R.string.sync_provider_authority), true); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // we can enable inexact timers in our periodic sync SyncRequest request = new SyncRequest.Builder().syncPeriodic(syncInterval, flexTime) .setSyncAdapter(account, authority).setExtras(new Bundle()).build(); ContentResolver.requestSync(request); } else { ContentResolver.addPeriodicSync(account, authority, new Bundle(), syncInterval); } } } /** * Helper method to have the sync adapter sync immediately * * @param context The context used to access the account service */ public static void syncImmediately(Context context) { Bundle bundle = new Bundle(); bundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); ContentResolver.requestSync(getSyncAccount(context), context.getString(R.string.sync_provider_authority), bundle); } /** * Broadcast Message to widgets to update data */ private void updateWidgets() { // Send intent to the Widget to notify that the data was updated Intent dataUpdated = new Intent(ACTION_DATA_UPDATE) // Ensures that only components in the app will receive the broadcast .setPackage(getContext().getPackageName()); getContext().sendBroadcast(dataUpdated); } /** * Raises a notification with a biggest earthquake with each sync * * @param notifyValues data with the biggest recent earthquake */ private void sendNotification(ContentValues notifyValues) { Context context = getContext(); if (Utilities.getNotificationsPrefs(context) && !Utilities.checkForeground(context)) { //checking the last update and notify if it' the first of the day SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); String lastNotificationKey = context.getString(R.string.sharedprefs_key_lastnotification); long lastSync = prefs.getLong(lastNotificationKey, 0); if (System.currentTimeMillis() - lastSync >= DateUtils.DAY_IN_MILLIS) { Intent intent = new Intent(context, DetailActivity.class); double latitude = notifyValues.getAsDouble(EarthquakeColumns.LATITUDE); double longitude = notifyValues.getAsDouble(EarthquakeColumns.LONGITUDE); LatLng latLng = new LatLng(latitude, longitude); String distance = context.getString(R.string.earthquake_distance, LocationUtils.getDistance(latLng, LocationUtils.getLocation(context))); String magnitude = context.getString(R.string.earthquake_magnitude, notifyValues.getAsDouble(EarthquakeColumns.MAG)); String date = Utilities.getRelativeDate(notifyValues.getAsLong(EarthquakeColumns.TIME)); double depth = notifyValues.getAsDouble(EarthquakeColumns.DEPTH); intent.putExtra(Feature.MAGNITUDE, notifyValues.getAsDouble(EarthquakeColumns.MAG)); intent.putExtra(Feature.PLACE, notifyValues.getAsString(EarthquakeColumns.PLACE)); intent.putExtra(Feature.DATE, date); intent.putExtra(Feature.LINK, notifyValues.getAsString(EarthquakeColumns.URL)); intent.putExtra(Feature.LATLNG, latLng); intent.putExtra(Feature.DISTANCE, distance); intent.putExtra(Feature.DEPTH, depth); intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); PendingIntent pendingIntent = PendingIntent.getActivity(context, EarthquakeObject.NOTIFICATION_ID_1, intent, PendingIntent.FLAG_UPDATE_CURRENT); NotificationCompat.Builder builder = new NotificationCompat.Builder(context); Bitmap largeIcon = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher); builder.setDefaults(Notification.DEFAULT_ALL).setAutoCancel(true) .setContentTitle(context.getString(R.string.earthquake_statistics_largest)) .setContentText(context.getString(R.string.earthquake_magnitude, notifyValues.get(EarthquakeColumns.MAG))) .setContentIntent(pendingIntent).setSmallIcon(R.drawable.ic_info_black_24dp) .setLargeIcon(largeIcon).setTicker(context.getString(R.string.app_name)) .setStyle(new NotificationCompat.BigTextStyle() .bigText(notifyValues.get(EarthquakeColumns.PLACE).toString() + "\n" + magnitude + "\n" + context.getString(R.string.earthquake_depth, depth) + "\n" + distance + "\n" + date)) .setGroup(EarthquakeObject.NOTIFICATION_GROUP).setGroupSummary(true); NotificationManagerCompat managerCompat = NotificationManagerCompat.from(context); managerCompat.notify(EarthquakeObject.NOTIFICATION_ID_1, builder.build()); //refreshing last sync boolean success = prefs.edit().putLong(lastNotificationKey, System.currentTimeMillis()).commit(); } } } }