/** * Copyright (C) 2016 eBusiness Information * * This file is part of OSM Contributor. * * OSM Contributor is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * OSM Contributor is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with OSM Contributor. If not, see <http://www.gnu.org/licenses/>. */ package io.jawg.osmcontributor.service; import android.app.IntentService; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Intent; import android.support.v4.app.NotificationCompat; import android.util.Log; import com.mapbox.mapboxsdk.geometry.LatLng; import com.mapbox.mapboxsdk.geometry.LatLngBounds; import com.mapbox.mapboxsdk.offline.OfflineRegion; import com.mapbox.mapboxsdk.offline.OfflineRegionError; import com.mapbox.mapboxsdk.offline.OfflineRegionStatus; import com.mapbox.mapboxsdk.offline.OfflineTilePyramidRegionDefinition; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.inject.Inject; import io.jawg.osmcontributor.BuildConfig; import io.jawg.osmcontributor.OsmTemplateApplication; import io.jawg.osmcontributor.R; import io.jawg.osmcontributor.offline.OfflineRegionManager; import io.jawg.osmcontributor.offline.events.CancelOfflineRegionDownloadEvent; import io.jawg.osmcontributor.offline.events.OfflineRegionCreatedEvent; /** * @author Tommy Buonomo on 03/08/16. */ public class OfflineRegionDownloadService extends IntentService { private static final String TAG = "MapOfflineManager"; public static final String LIST_PARAM = "LIST"; public static final String SIZE_PARAM = "SIZE_PARAM"; public static final String REGION_NAME_PARAM = "REGION_NAME"; private static final String CANCEL_DOWNLOAD = "CANCEL_DOWNLOAD"; public static final int MIN_ZOOM = 13; private static final int MAX_ZOOM = 20; private List<OfflineRegion> waitingOfflineRegions; private OfflineRegion currentDownloadRegion; private boolean deliverStatusUpdate; private Intent intent; private Map<String, NotificationCompat.Builder> notifications; @Inject OfflineRegionManager offlineRegionManager; @Inject EventBus eventBus; @Inject NotificationManager notificationManager; public OfflineRegionDownloadService() { super(OfflineRegionDownloadService.class.getSimpleName()); } @Override public void onCreate() { super.onCreate(); ((OsmTemplateApplication) getApplication()).getOsmTemplateComponent().inject(this); eventBus.register(this); waitingOfflineRegions = new ArrayList<>(); notifications = new HashMap<>(); } @Override protected void onHandleIntent(final Intent intent) { this.intent = intent; offlineRegionManager.listOfflineRegions(new OfflineRegionManager.OnOfflineRegionsListedListener() { @Override public void onOfflineRegionsListed(List<OfflineRegion> offlineRegions) { startDownloadIfNeeded(intent, offlineRegions); } }); } private void startDownloadIfNeeded(Intent intent, final List<OfflineRegion> presentOfflineRegions) { if (intent == null) { return; } final int size = intent.getIntExtra(SIZE_PARAM, -1); if (size != -1) { int c = 0; // There is some regions to download for (int i = 0; i < size; i++) { ArrayList<String> areasString = intent.getStringArrayListExtra(LIST_PARAM + i); LatLngBounds bounds = convertToLatLngBounds(areasString); OfflineRegion presentOfflineRegion = containsInOfflineRegion(presentOfflineRegions, bounds); if (presentOfflineRegion == null) { // The region has never been downloaded String regionName = intent.getStringExtra(REGION_NAME_PARAM); regionName = regionName == null ? "Region " + (presentOfflineRegions.size() + c) : regionName; c++; downloadOfflineRegion(bounds, regionName); } else { //The region is already downloaded, we check if it was completed checkIfRegionDownloadIsCompleted(presentOfflineRegion); } } } } private void checkIfRegionDownloadIsCompleted(final OfflineRegion offlineRegion) { offlineRegion.getStatus(new OfflineRegion.OfflineRegionStatusCallback() { @Override public void onStatus(OfflineRegionStatus status) { if (!status.isComplete() && status.getDownloadState() != OfflineRegion.STATE_ACTIVE) { resumeDownloadOfflineRegion(offlineRegion); } } @Override public void onError(String error) { Log.e(TAG, error); } }); } public void downloadOfflineRegion(LatLngBounds latLngBounds, final String regionName) { final OfflineTilePyramidRegionDefinition definition = new OfflineTilePyramidRegionDefinition( BuildConfig.MAP_STYLE_URL, latLngBounds, MIN_ZOOM, MAX_ZOOM, this.getResources().getDisplayMetrics().density); // Build the notification buildNotification(regionName); offlineRegionManager.createOfflineRegion(definition, regionName, new OfflineRegionManager.OnOfflineRegionCreatedListener() { @Override public void onOfflineRegionCreated(OfflineRegion offlineRegion, String regionName) { // Monitor the download progress using setObserver offlineRegion.setObserver(getOfflineRegionObserver(regionName)); startDownloadOfflineRegion(offlineRegion); eventBus.post(new OfflineRegionCreatedEvent()); } }); } private void resumeDownloadOfflineRegion(OfflineRegion offlineRegion) { Log.d(TAG, "resumeDownloadOfflineRegion: " + offlineRegion); String regionName = OfflineRegionManager.decodeRegionName(offlineRegion.getMetadata()); buildNotification(regionName); offlineRegion.setObserver(getOfflineRegionObserver(regionName)); startDownloadOfflineRegion(offlineRegion); } private void startDownloadOfflineRegion(OfflineRegion offlineRegion) { // An area is already downloading, we put the area in waiting if (currentDownloadRegion != null) { waitingOfflineRegions.add(offlineRegion); } else { currentDownloadRegion = offlineRegion; offlineRegion.setDownloadState(OfflineRegion.STATE_ACTIVE); deliverStatusUpdate = true; } } private synchronized void checkNextDownload() { if (!waitingOfflineRegions.isEmpty()) { startDownloadOfflineRegion(waitingOfflineRegions.get(0)); waitingOfflineRegions.remove(0); } } private void cancelDownloadOfflineRegion() { if (currentDownloadRegion != null) { currentDownloadRegion.setDownloadState(OfflineRegion.STATE_INACTIVE); deliverStatusUpdate = false; currentDownloadRegion = null; } } private OfflineRegion.OfflineRegionObserver getOfflineRegionObserver(final String regionName) { final NotificationCompat.Builder builder = notifications.get(regionName); return new OfflineRegion.OfflineRegionObserver() { int percentage; @Override public void onStatusChanged(OfflineRegionStatus status) { if (deliverStatusUpdate) { // Calculate the download percentage and update the progress bar int newPercent = (int) Math.round(status.getRequiredResourceCount() >= 0 ? (100.0 * status.getCompletedResourceCount() / status.getRequiredResourceCount()) : 0.0); if (newPercent != percentage) { percentage = newPercent; builder.setContentText(percentage + "%"); builder.setProgress(100, percentage, false); notificationManager.notify(regionName.hashCode(), builder.build()); } } if (status.isComplete()) { // Download complete // When the loop is finished, updates the notification builder.setContentText("Region downloaded successfully") .setProgress(0, 0, false) .mActions .clear(); notificationManager.notify(regionName.hashCode(), builder.build()); currentDownloadRegion = null; checkNextDownload(); } } @Override public void onError(OfflineRegionError error) { // If an error occurs, print to logcat Log.e(TAG, "onError reason: " + error.getReason()); Log.e(TAG, "onError message: " + error.getMessage()); } @Override public void mapboxTileCountLimitExceeded(long limit) { // Notify if offline region exceeds maximum tile count Log.e(TAG, "Mapbox tile count limit exceeded: " + limit); } }; } /** * Build the download notification to display * * @param regionName * @return */ public void buildNotification(String regionName) { Intent cancelButtonIntent = new Intent(getApplicationContext(), CancelButtonReceiver.class); cancelButtonIntent.putExtra(CancelButtonReceiver.MAP_TAG_PARAM, regionName); cancelButtonIntent.setAction(CANCEL_DOWNLOAD + regionName.hashCode()); PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, cancelButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT); NotificationCompat.Builder builder = new NotificationCompat.Builder(this) .setSmallIcon(R.drawable.ic_file_download_white) .setContentTitle(this.getString(R.string.notification_download_title) + " " + regionName) .addAction(new NotificationCompat.Action(R.drawable.ic_clear_white, getString(R.string.cancel), pendingIntent)) .setDeleteIntent(pendingIntent); notifications.put(regionName, builder); } /** * Check if a region is present in the list with the bounds parameter. * @param regions * @param bounds * @return the OfflineRegion if it's present or null. */ private OfflineRegion containsInOfflineRegion(List<OfflineRegion> regions, LatLngBounds bounds) { for (OfflineRegion offlineRegion : regions) { if (((OfflineTilePyramidRegionDefinition) offlineRegion.getDefinition()).getBounds().equals(bounds)) { return offlineRegion; } } return null; } private LatLngBounds convertToLatLngBounds(List<String> latLngBoundsStrings) { if (latLngBoundsStrings.size() == 4) { return new LatLngBounds.Builder() .include(new LatLng(Double.parseDouble(latLngBoundsStrings.get(0)), Double.parseDouble(latLngBoundsStrings.get(1)))) .include(new LatLng(Double.parseDouble(latLngBoundsStrings.get(2)), Double.parseDouble(latLngBoundsStrings.get(3)))) .build(); } return null; } @Subscribe(threadMode = ThreadMode.MAIN) @SuppressWarnings("unused") public void onCancelOfflineRegionEvent(final CancelOfflineRegionDownloadEvent event) { if (currentDownloadRegion != null && OfflineRegionManager .decodeRegionName(currentDownloadRegion.getMetadata()) .equals(event.getRegionName())) { cancelDownloadOfflineRegion(); } } @Override public void onDestroy() { super.onDestroy(); } }