/* * Copyright (c) 2013, Will Szumski * Copyright (c) 2013, Doug Szumski * * This file is part of Cyclismo. * * Cyclismo 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. * * Cyclismo 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 Cyclismo. If not, see <http://www.gnu.org/licenses/>. */ /* * Copyright 2011 Google Inc. * * 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 org.cowboycoders.cyclismo.widgets; import android.annotation.TargetApi; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.SystemClock; import android.support.v4.app.TaskStackBuilder; import android.util.SparseIntArray; import android.view.View; import android.widget.RemoteViews; import org.cowboycoders.cyclismo.R; import org.cowboycoders.cyclismo.TrackDetailActivity; import org.cowboycoders.cyclismo.TrackListActivity; import org.cowboycoders.cyclismo.content.MyTracksProviderUtils; import org.cowboycoders.cyclismo.content.Track; import org.cowboycoders.cyclismo.services.ControlRecordingService; import org.cowboycoders.cyclismo.stats.TripStatistics; import org.cowboycoders.cyclismo.util.IntentUtils; import org.cowboycoders.cyclismo.util.PreferencesUtils; import org.cowboycoders.cyclismo.util.StringUtils; /** * A track widget to start/stop/pause/resume recording, launch My Tracks, and * display track statistics (total distance, total time, average speed, and * moving time) for the recording track, the selected track or the last track. * * @author Sandor Dornbush * @author Paul R. Saxman */ public class TrackWidgetProvider extends AppWidgetProvider { private static final int TWO_CELLS = 110; private static final int THREE_CELLS = 180; private static final int FOUR_CELLS = 250; // Array of appwidget id to height size in cells private static final SparseIntArray HEIGHT_SIZE = new SparseIntArray(); private static final int DEFAULT_SIZE = 2; private static final int[] ITEM1_IDS = { R.id.track_widget_item1_label, R.id.track_widget_item1_value, R.id.track_widget_item1_unit, R.id.track_widget_item1_chronometer }; private static final int[] ITEM2_IDS = { R.id.track_widget_item2_label, R.id.track_widget_item2_value, R.id.track_widget_item2_unit, R.id.track_widget_item2_chronometer }; private static final int[] ITEM3_IDS = { R.id.track_widget_item3_label, R.id.track_widget_item3_value, R.id.track_widget_item3_unit, R.id.track_widget_item3_chronometer }; private static final int[] ITEM4_IDS = { R.id.track_widget_item4_label, R.id.track_widget_item4_value, R.id.track_widget_item4_unit, R.id.track_widget_item4_chronometer }; @Override public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); String action = intent.getAction(); if (context.getString(R.string.track_paused_broadcast_action).equals(action) || context.getString(R.string.track_resumed_broadcast_action).equals(action) || context.getString(R.string.track_started_broadcast_action).equals(action) || context.getString(R.string.track_stopped_broadcast_action).equals(action) || context.getString(R.string.track_update_broadcast_action).equals(action)) { long trackId = intent.getLongExtra(context.getString(R.string.track_id_broadcast_extra), -1L); AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); int[] appWidgetIds = appWidgetManager.getAppWidgetIds( new ComponentName(context, TrackWidgetProvider.class)); for (int appWidgetId : appWidgetIds) { RemoteViews remoteViews = getRemoteViews( context, trackId, HEIGHT_SIZE.get(appWidgetId, DEFAULT_SIZE)); appWidgetManager.updateAppWidget(appWidgetId, remoteViews); } } } @TargetApi(16) @Override public void onAppWidgetOptionsChanged( Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) { if (newOptions != null) { int size; int height = newOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT); if (height == 0) { size = 2; } else if (height >= FOUR_CELLS) { size = 4; } else if (height >= THREE_CELLS) { size = 3; } else if (height >= TWO_CELLS) { size = 2; } else { size = 1; } HEIGHT_SIZE.put(appWidgetId, size); RemoteViews remoteViews = getRemoteViews(context, -1L, size); appWidgetManager.updateAppWidget(appWidgetId, remoteViews); } } /** * Updates an app widget. * * @param context the context * @param appWidgetManager the app widget manager * @param appWidgetId the app widget id */ public static void updateAppWidget( Context context, AppWidgetManager appWidgetManager, int appWidgetId) { RemoteViews remoteViews = getRemoteViews( context, -1L, HEIGHT_SIZE.get(appWidgetId, DEFAULT_SIZE)); appWidgetManager.updateAppWidget(appWidgetId, remoteViews); } /** * Gets the remote views. * * @param context the context * @param trackId the track id * @param heightSize the layout height size */ private static RemoteViews getRemoteViews(Context context, long trackId, int heightSize) { int layout; switch (heightSize) { case 4: layout = R.layout.track_widget_4x4; break; case 3: layout = R.layout.track_widget_4x3; break; case 2: layout = R.layout.track_widget_4x2; break; case 1: layout = R.layout.track_widget_4x1; break; default: layout = R.layout.track_widget_4x2; break; } RemoteViews remoteViews = new RemoteViews(context.getPackageName(), layout); // Get the preferences long recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key); boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; boolean isPaused = PreferencesUtils.getBoolean(context, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT); boolean metricUnits = PreferencesUtils.getBoolean( context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT); boolean reportSpeed = PreferencesUtils.getBoolean( context, R.string.report_speed_key, PreferencesUtils.REPORT_SPEED_DEFAULT); int item1 = PreferencesUtils.getInt( context, R.string.track_widget_item1, PreferencesUtils.TRACK_WIDGET_ITEM1_DEFAULT); int item2 = PreferencesUtils.getInt( context, R.string.track_widget_item2, PreferencesUtils.TRACK_WIDGET_ITEM2_DEFAULT); // Get track and trip statistics MyTracksProviderUtils myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context); if (trackId == -1L) { trackId = recordingTrackId; } Track track = trackId != -1L ? myTracksProviderUtils.getTrack(trackId) : myTracksProviderUtils.getLastTrack(); TripStatistics tripStatistics = track == null ? null : track.getTripStatistics(); updateStatisticsContainer(context, remoteViews, track); setItem(context, remoteViews, ITEM1_IDS, item1, tripStatistics, isRecording, isPaused, metricUnits, reportSpeed); setItem(context, remoteViews, ITEM2_IDS, item2, tripStatistics, isRecording, isPaused, metricUnits, reportSpeed); updateRecordButton(context, remoteViews, isRecording, isPaused); updateStopButton(context, remoteViews, isRecording); if (heightSize > 1) { int item3 = PreferencesUtils.getInt( context, R.string.track_widget_item3, PreferencesUtils.TRACK_WIDGET_ITEM3_DEFAULT); int item4 = PreferencesUtils.getInt( context, R.string.track_widget_item4, PreferencesUtils.TRACK_WIDGET_ITEM4_DEFAULT); setItem(context, remoteViews, ITEM3_IDS, item3, tripStatistics, isRecording, isPaused, metricUnits, reportSpeed); setItem(context, remoteViews, ITEM4_IDS, item4, tripStatistics, isRecording, isPaused, metricUnits, reportSpeed); updateRecordStatus(context, remoteViews, isRecording, isPaused); } return remoteViews; } /** * Sets a widget item. * * @param context the context * @param remoteViews the remote view * @param ids the item's ids * @param value the item value * @param tripStatistics the trip statistics * @param metricUnits true to use metric units * @param reportSpeed try to report speed */ private static void setItem(Context context, RemoteViews remoteViews, int[] ids, int value, TripStatistics tripStatistics, boolean isRecording, boolean isPaused, boolean metricUnits, boolean reportSpeed) { switch (value) { case 0: updateDistance(context, remoteViews, ids, tripStatistics, metricUnits); break; case 1: updateTotalTime(context, remoteViews, ids, tripStatistics, isRecording, isPaused); break; case 2: updateAverageSpeed(context, remoteViews, ids, tripStatistics, metricUnits, reportSpeed); break; case 3: updateMovingTime(context, remoteViews, ids, tripStatistics); break; case 4: updateAverageMovingSpeed( context, remoteViews, ids, tripStatistics, metricUnits, reportSpeed); break; default: updateDistance(context, remoteViews, ids, tripStatistics, metricUnits); break; } if (value != 1) { remoteViews.setViewVisibility(ids[1], View.VISIBLE); remoteViews.setViewVisibility(ids[2], View.VISIBLE); remoteViews.setViewVisibility(ids[3], View.GONE); remoteViews.setChronometer(ids[3], SystemClock.elapsedRealtime(), null, false); } } /** * Updates the statistics container. * * @param context the context * @param remoteViews the remote views * @param track the track */ private static void updateStatisticsContainer( Context context, RemoteViews remoteViews, Track track) { Intent intent; if (track != null) { intent = IntentUtils.newIntent(context, TrackDetailActivity.class) .putExtra(TrackDetailActivity.EXTRA_TRACK_ID, track.getId()); } else { intent = IntentUtils.newIntent(context, TrackListActivity.class); } TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context); taskStackBuilder.addNextIntent(intent); PendingIntent pendingIntent = taskStackBuilder.getPendingIntent(0, 0); remoteViews.setOnClickPendingIntent(R.id.track_widget_stats_container, pendingIntent); } /** * Updates distance. * * @param context the context * @param remoteViews the remote views * @param ids the item's ids * @param tripStatistics the trip statistics * @param metricUnits true to use metric units */ private static void updateDistance(Context context, RemoteViews remoteViews, int[] ids, TripStatistics tripStatistics, boolean metricUnits) { double totalDistance = tripStatistics == null ? Double.NaN : tripStatistics.getTotalDistance(); String[] totalDistanceParts = StringUtils.getDistanceParts(context, totalDistance, metricUnits); remoteViews.setTextViewText(ids[0], context.getString(R.string.stats_distance)); remoteViews.setTextViewText(ids[1], totalDistanceParts[0]); remoteViews.setTextViewText(ids[2], totalDistanceParts[1]); } /** * Updates total time. * * @param context the context * @param remoteViews the remote views * @param ids the item's ids * @param tripStatistics the trip statistics */ private static void updateTotalTime(Context context, RemoteViews remoteViews, int[] ids, TripStatistics tripStatistics, boolean isRecording, boolean isPaused) { if (isRecording && !isPaused && tripStatistics != null) { long time = tripStatistics.getTotalTime() + System.currentTimeMillis() - tripStatistics.getStopTime(); remoteViews.setChronometer(ids[3], SystemClock.elapsedRealtime() - time, null, true); remoteViews.setViewVisibility(ids[1], View.GONE); remoteViews.setViewVisibility(ids[2], View.GONE); remoteViews.setViewVisibility(ids[3], View.VISIBLE); } else { remoteViews.setChronometer(ids[3], SystemClock.elapsedRealtime(), null, false); remoteViews.setViewVisibility(ids[1], View.VISIBLE); remoteViews.setViewVisibility(ids[2], View.GONE); remoteViews.setViewVisibility(ids[3], View.GONE); String totalTime = tripStatistics == null ? context.getString(R.string.value_unknown) : StringUtils.formatElapsedTime(tripStatistics.getTotalTime()); remoteViews.setTextViewText(ids[0], context.getString(R.string.stats_total_time)); remoteViews.setTextViewText(ids[1], totalTime); } } /** * Updates average speed. * * @param context the context * @param remoteViews the remote views * @param ids the item's ids * @param tripStatistics the trip statistics * @param metricUnits true to use metric units * @param reportSpeed true to report speed */ private static void updateAverageSpeed(Context context, RemoteViews remoteViews, int[] ids, TripStatistics tripStatistics, boolean metricUnits, boolean reportSpeed) { String averageSpeedLabel = context.getString( reportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace); remoteViews.setTextViewText(ids[0], averageSpeedLabel); Double speed = tripStatistics == null ? Double.NaN : tripStatistics.getAverageSpeed(); String[] speedParts = StringUtils.getSpeedParts(context, speed, metricUnits, reportSpeed); remoteViews.setTextViewText(ids[1], speedParts[0]); remoteViews.setTextViewText(ids[2], speedParts[1]); } /** * Updates moving time. * * @param context the context * @param remoteViews the remote views * @param ids the item's ids * @param tripStatistics the trip statistics */ private static void updateMovingTime( Context context, RemoteViews remoteViews, int[] ids, TripStatistics tripStatistics) { String movingTime = tripStatistics == null ? context.getString(R.string.value_unknown) : StringUtils.formatElapsedTime(tripStatistics.getMovingTime()); remoteViews.setTextViewText(ids[0], context.getString(R.string.stats_moving_time)); remoteViews.setTextViewText(ids[1], movingTime); remoteViews.setViewVisibility(ids[2], View.GONE); } /** * Updates average moving speed. * * @param context the context * @param remoteViews the remote views * @param ids the item's ids * @param tripStatistics the trip statistics * @param metricUnits true to use metric units * @param reportSpeed true to report speed */ private static void updateAverageMovingSpeed(Context context, RemoteViews remoteViews, int[] ids, TripStatistics tripStatistics, boolean metricUnits, boolean reportSpeed) { String averageMovingSpeedLabel = context.getString( reportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace); remoteViews.setTextViewText(ids[0], averageMovingSpeedLabel); Double speed = tripStatistics == null ? Double.NaN : tripStatistics.getAverageMovingSpeed(); String[] speedParts = StringUtils.getSpeedParts(context, speed, metricUnits, reportSpeed); remoteViews.setTextViewText(ids[1], speedParts[0]); remoteViews.setTextViewText(ids[2], speedParts[1]); } /** * Updates the record button. * * @param context the context * @param remoteViews the remote views * @param isRecording true if recording * @param recordingTrackPaused true if recording track is paused */ private static void updateRecordButton( Context context, RemoteViews remoteViews, boolean isRecording, boolean recordingTrackPaused) { remoteViews.setImageViewResource(R.id.track_widget_record_button, isRecording && !recordingTrackPaused ? R.drawable.btn_pause : R.drawable.btn_record); int recordActionId; if (isRecording) { recordActionId = recordingTrackPaused ? R.string.track_action_resume : R.string.track_action_pause; } else { recordActionId = R.string.track_action_start; } Intent intent = new Intent(context, ControlRecordingService.class).setAction( context.getString(recordActionId)); PendingIntent pendingIntent = PendingIntent.getService( context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); remoteViews.setOnClickPendingIntent(R.id.track_widget_record_button, pendingIntent); } /** * Updates the stop button. * * @param context the context * @param remoteViews the remote views * @param isRecording true if recording */ private static void updateStopButton( Context context, RemoteViews remoteViews, boolean isRecording) { remoteViews.setImageViewResource( R.id.track_widget_stop_button, isRecording ? R.drawable.btn_stop_1 : R.drawable.btn_stop_0); remoteViews.setBoolean(R.id.track_widget_stop_button, "setEnabled", isRecording); if (isRecording) { Intent intent = new Intent(context, ControlRecordingService.class).setAction( context.getString(R.string.track_action_end)); PendingIntent pendingIntent = PendingIntent.getService( context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); remoteViews.setOnClickPendingIntent(R.id.track_widget_stop_button, pendingIntent); } } /** * Updates recording status. * * @param context the context * @param remoteViews the remote views * @param isRecording true if recording * @param recordingTrackPaused true if recording track is paused */ private static void updateRecordStatus( Context context, RemoteViews remoteViews, boolean isRecording, boolean recordingTrackPaused) { String status; int colorId; if (isRecording) { status = context.getString( recordingTrackPaused ? R.string.generic_paused : R.string.generic_recording); colorId = recordingTrackPaused ? android.R.color.white : R.color.red; } else { status = ""; colorId = android.R.color.white; } remoteViews.setTextColor( R.id.track_widget_record_status, context.getResources().getColor(colorId)); remoteViews.setTextViewText(R.id.track_widget_record_status, status); } }