summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml31
-rw-r--r--res/drawable-nodpi/appwidget_city_clock_preview.pngbin0 -> 11487 bytes
-rw-r--r--res/drawable-nodpi/appwidget_digital_clock_preview.pngbin11323 -> 15661 bytes
-rw-r--r--res/layout/city_widget.xml65
-rw-r--r--res/layout/widget_city_list_item.xml52
-rw-r--r--res/values-sw600dp/dimens.xml3
-rw-r--r--res/values-v23/styles.xml1
-rw-r--r--res/values/dimens.xml5
-rw-r--r--res/values/donottranslate.xml2
-rw-r--r--res/values/donottranslate_events.xml3
-rw-r--r--res/values/strings.xml11
-rw-r--r--res/xml/city_appwidget.xml28
-rw-r--r--src/com/android/alarmclock/AnalogAppWidgetProvider.java53
-rw-r--r--src/com/android/alarmclock/CityAppWidgetProvider.java526
-rw-r--r--src/com/android/alarmclock/DigitalAppWidgetProvider.java40
-rw-r--r--src/com/android/deskclock/DeskClockApplication.java2
-rw-r--r--src/com/android/deskclock/Utils.java32
-rw-r--r--src/com/android/deskclock/data/CityModel.java8
-rw-r--r--src/com/android/deskclock/data/DataModel.java36
-rw-r--r--src/com/android/deskclock/data/WidgetDAO.java86
-rw-r--r--src/com/android/deskclock/data/WidgetModel.java88
-rw-r--r--src/com/android/deskclock/worldclock/CitySelectionActivity.java2
-rw-r--r--src/com/android/deskclock/worldclock/WidgetCitySelectionActivity.java439
23 files changed, 1470 insertions, 43 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 3c675c3a8..f5bd34fa2 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -63,7 +63,6 @@
<activity
android:name=".DeskClock"
- android:icon="@mipmap/ic_launcher_alarmclock"
android:label="@string/app_label"
android:launchMode="singleTask"
android:theme="@style/DeskClockTheme"
@@ -78,7 +77,6 @@
<activity-alias
android:name=".DockClock"
android:enabled="false"
- android:icon="@mipmap/ic_launcher_alarmclock"
android:label="@string/app_label"
android:launchMode="singleTask"
android:targetActivity="DeskClock"
@@ -113,6 +111,17 @@
</activity>
<activity
+ android:name=".worldclock.WidgetCitySelectionActivity"
+ android:excludeFromRecents="true"
+ android:label="@string/cities_activity_title"
+ android:taskAffinity=""
+ android:theme="@style/CitiesTheme">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
+ </intent-filter>
+ </activity>
+
+ <activity
android:name=".alarms.AlarmActivity"
android:excludeFromRecents="true"
android:showOnLockScreen="true"
@@ -259,7 +268,6 @@
<receiver
android:name="com.android.alarmclock.AnalogAppWidgetProvider"
- android:icon="@mipmap/ic_launcher_alarmclock"
android:label="@string/analog_gadget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -275,7 +283,6 @@
<receiver
android:name="com.android.alarmclock.DigitalAppWidgetProvider"
- android:icon="@mipmap/ic_launcher_alarmclock"
android:label="@string/digital_gadget">
<intent-filter>
<action android:name="android.intent.action.SCREEN_ON" />
@@ -290,6 +297,22 @@
android:resource="@xml/digital_appwidget" />
</receiver>
+ <receiver
+ android:name="com.android.alarmclock.CityAppWidgetProvider"
+ android:label="@string/city_gadget">
+ <intent-filter>
+ <action android:name="android.intent.action.TIME_SET" />
+ <action android:name="android.intent.action.SCREEN_ON" />
+ <action android:name="android.intent.action.DATE_CHANGED" />
+ <action android:name="android.intent.action.LOCALE_CHANGED" />
+ <action android:name="android.intent.action.TIMEZONE_CHANGED" />
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+ </intent-filter>
+ <meta-data
+ android:name="android.appwidget.provider"
+ android:resource="@xml/city_appwidget" />
+ </receiver>
+
<!-- Dream (screensaver) implementation -->
<service
android:name=".Screensaver"
diff --git a/res/drawable-nodpi/appwidget_city_clock_preview.png b/res/drawable-nodpi/appwidget_city_clock_preview.png
new file mode 100644
index 000000000..176b547f3
--- /dev/null
+++ b/res/drawable-nodpi/appwidget_city_clock_preview.png
Binary files differ
diff --git a/res/drawable-nodpi/appwidget_digital_clock_preview.png b/res/drawable-nodpi/appwidget_digital_clock_preview.png
index 88c84a3c1..e0922a14a 100644
--- a/res/drawable-nodpi/appwidget_digital_clock_preview.png
+++ b/res/drawable-nodpi/appwidget_digital_clock_preview.png
Binary files differ
diff --git a/res/layout/city_widget.xml b/res/layout/city_widget.xml
new file mode 100644
index 000000000..40592b91f
--- /dev/null
+++ b/res/layout/city_widget.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/city_widget"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:orientation="vertical">
+
+ <TextClock
+ android:id="@+id/clock"
+ style="@style/widget_big_thin"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal|top"
+ android:ellipsize="none"
+ android:includeFontPadding="false"
+ android:singleLine="true"
+ android:textColor="@color/clock_white" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal|top">
+
+ <TextView
+ android:id="@+id/city_name"
+ style="@style/widget_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:includeFontPadding="false"
+ android:singleLine="true"
+ android:textAllCaps="true"
+ android:textColor="@color/clock_white" />
+
+ <TextClock
+ android:id="@+id/city_day"
+ style="@style/widget_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="none"
+ android:includeFontPadding="false"
+ android:singleLine="true"
+ android:textAllCaps="true"
+ android:textColor="@color/clock_white" />
+
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/widget_city_list_item.xml b/res/layout/widget_city_list_item.xml
new file mode 100644
index 000000000..98704821b
--- /dev/null
+++ b/res/layout/widget_city_list_item.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:gravity="center_vertical"
+ android:minHeight="@dimen/cities_list_item_height">
+
+ <TextView
+ android:id="@+id/index"
+ android:layout_width="74dp"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:textColor="@color/clock_white"
+ android:textSize="@dimen/label_text_size"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/city_name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:paddingEnd="10dp"
+ android:paddingStart="16dp"
+ android:singleLine="true"
+ android:textAppearance="@style/PrimaryLabelTextAppearance" />
+
+ <TextView
+ android:id="@+id/city_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dp"
+ android:textAppearance="@style/SecondaryLabelTextAppearance" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/values-sw600dp/dimens.xml b/res/values-sw600dp/dimens.xml
index a1ffa8c02..b59447268 100644
--- a/res/values-sw600dp/dimens.xml
+++ b/res/values-sw600dp/dimens.xml
@@ -90,5 +90,8 @@
<dimen name="min_digital_widget_width">300dp</dimen>
<dimen name="min_digital_widget_height">170dp</dimen>
+ <!-- The fixed size of the font for the city name / day of week in the city widget. -->
+ <dimen name="city_widget_name_font_size">20dp</dimen>
+
<dimen name="footer_button_size">80dip</dimen>
</resources>
diff --git a/res/values-v23/styles.xml b/res/values-v23/styles.xml
index 68e892d50..f55c40e46 100644
--- a/res/values-v23/styles.xml
+++ b/res/values-v23/styles.xml
@@ -9,6 +9,7 @@
</style>
<style name="widget_label" parent="label">
+ <item name="android:fontFamily">sans-serif-light</item>
<item name="android:textAllCaps">true</item>
<item name="android:letterSpacing">0.15</item>
<item name="android:shadowRadius">@dimen/digital_widget_shadow_radius</item>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index d97da2aed..a77b77bb7 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -131,6 +131,11 @@
<dimen name="min_digital_widget_width">206dp</dimen>
<dimen name="min_digital_widget_height">129dp</dimen>
+ <!-- The fixed size of the font for the city name / day of week in the city widget. -->
+ <dimen name="city_widget_name_font_size">12dp</dimen>
+ <!-- The maximum size of the font for the time in the city widget. -->
+ <dimen name="city_widget_max_clock_font_size">104dp</dimen>
+
<!-- shadow styles for digital widget text -->
<item name="digital_widget_shadow_radius" format="float" type="dimen">2.75</item>
<item name="digital_widget_shadow_dy" format="float" type="dimen">2.0</item>
diff --git a/res/values/donottranslate.xml b/res/values/donottranslate.xml
index 29c61e44c..4fb3d0685 100644
--- a/res/values/donottranslate.xml
+++ b/res/values/donottranslate.xml
@@ -15,6 +15,8 @@
-->
<resources>
+ <!-- Format for displaying the day of week with a preceding slash divider. -->
+ <string name="abbrev_wday">&#160;/ EEE</string>
<!-- String matching the lock screen format for displaying the date. -->
<string name="abbrev_wday_month_day_no_year">EEEMMMd</string>
<!-- Format for describing the date, for accessibility. -->
diff --git a/res/values/donottranslate_events.xml b/res/values/donottranslate_events.xml
index 5e641622b..d7f203965 100644
--- a/res/values/donottranslate_events.xml
+++ b/res/values/donottranslate_events.xml
@@ -19,6 +19,9 @@
<string name="category_clock">Clock</string>
<string name="category_timer">Timer</string>
<string name="category_stopwatch">Stopwatch</string>
+ <string name="category_city_widget">City Widget</string>
+ <string name="category_analog_widget">Analog Widget</string>
+ <string name="category_digital_widget">Digital Widget</string>
<!-- Action names for events describe what type of manipulation was performed. -->
<string name="action_dismiss">Dismiss</string>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 10716ed4e..a5189f22d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -184,9 +184,15 @@
this character, i.e. "Mon, Tue, Wed" -->
<string name="day_concat">", "</string>
- <!-- Label for analog clock gadget displayed on-screen when that gadget is represented to the user. -->
+ <!-- Label for analog clock gadget displayed in the widget picker. [CHAR LIMIT=18] -->
<string name="analog_gadget">Analog clock</string>
+ <!-- Label for digital clock gadget displayed in the widget picker. [CHAR LIMIT=18] -->
+ <string name="digital_gadget">Digital clock</string>
+
+ <!-- Label for city clock gadget displayed in the widget picker. [CHAR LIMIT=18] -->
+ <string name="city_gadget">World clock</string>
+
<!-- Settings activity name -->
<!-- Label for the Settings activity displayed on-screen when that activity must be represented to the user. -->
<string name="settings">Settings</string>
@@ -748,9 +754,6 @@
<!-- Description of field showing the next alarm time in the clock page, for accessibility. -->
<string name="next_alarm_description">Next alarm: <xliff:g id="alarm_time" example="Wed 8:00am">%s</xliff:g></string>
- <!-- Label for digital clock gadget displayed on-screen when that gadget is represented to the user. -->
- <string name="digital_gadget">Digital clock</string>
-
<!-- String for no alarms -->
<string name="no_alarms">No Alarms</string>
diff --git a/res/xml/city_appwidget.xml b/res/xml/city_appwidget.xml
new file mode 100644
index 000000000..3dbbc88fe
--- /dev/null
+++ b/res/xml/city_appwidget.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ 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.
+-->
+
+<appwidget-provider
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:configure="com.android.deskclock.worldclock.WidgetCitySelectionActivity"
+ android:initialLayout="@layout/city_widget"
+ android:minHeight="59dp"
+ android:minWidth="136dp"
+ android:minResizeHeight="59dp"
+ android:minResizeWidth="136dp"
+ android:previewImage="@drawable/appwidget_city_clock_preview"
+ android:resizeMode="horizontal|vertical"
+ android:updatePeriodMillis="0"
+ android:widgetCategory="keyguard|home_screen" /> \ No newline at end of file
diff --git a/src/com/android/alarmclock/AnalogAppWidgetProvider.java b/src/com/android/alarmclock/AnalogAppWidgetProvider.java
index 1a252a82d..80bc22f37 100644
--- a/src/com/android/alarmclock/AnalogAppWidgetProvider.java
+++ b/src/com/android/alarmclock/AnalogAppWidgetProvider.java
@@ -16,35 +16,60 @@
package com.android.alarmclock;
-import com.android.deskclock.HandleDeskClockApiCalls;
-import com.android.deskclock.R;
-
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
-import android.content.BroadcastReceiver;
+import android.appwidget.AppWidgetProvider;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.widget.RemoteViews;
+import com.android.deskclock.DeskClock;
+import com.android.deskclock.R;
+import com.android.deskclock.Utils;
+import com.android.deskclock.data.DataModel;
+
/**
* Simple widget to show an analog clock.
*/
-public class AnalogAppWidgetProvider extends BroadcastReceiver {
+public class AnalogAppWidgetProvider extends AppWidgetProvider {
+
@Override
public void onReceive(Context context, Intent intent) {
- if (!AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(intent.getAction())) {
+ super.onReceive(context, intent);
+
+ final AppWidgetManager wm = AppWidgetManager.getInstance(context);
+ if (wm == null) {
return;
}
- final String packageName = context.getPackageName();
- final RemoteViews views = new RemoteViews(packageName, R.layout.analog_appwidget);
+ // Send events for newly created/deleted widgets.
+ final ComponentName provider = new ComponentName(context, getClass());
+ final int widgetCount = wm.getAppWidgetIds(provider).length;
- final Intent showClock = new Intent(HandleDeskClockApiCalls.ACTION_SHOW_CLOCK)
- .putExtra(HandleDeskClockApiCalls.EXTRA_EVENT_LABEL, R.string.label_widget);
- final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, showClock, 0);
- views.setOnClickPendingIntent(R.id.analog_appwidget, pendingIntent);
+ final DataModel dm = DataModel.getDataModel();
+ dm.updateWidgetCount(getClass(), widgetCount, R.string.category_analog_widget);
+ }
+
+ /**
+ * Called when widgets must provide remote views.
+ */
+ @Override
+ public void onUpdate(Context context, AppWidgetManager wm, int[] widgetIds) {
+ super.onUpdate(context, wm, widgetIds);
- final int[] appWidgetIds = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
- AppWidgetManager.getInstance(context).updateAppWidget(appWidgetIds, views);
+ for (int widgetId : widgetIds) {
+ final String packageName = context.getPackageName();
+ final RemoteViews widget = new RemoteViews(packageName, R.layout.analog_appwidget);
+
+ // Tapping on the widget opens the app (if not on the lock screen).
+ if (Utils.isWidgetClickable(wm, widgetId)) {
+ final Intent openApp = new Intent(context, DeskClock.class);
+ final PendingIntent pi = PendingIntent.getActivity(context, 0, openApp, 0);
+ widget.setOnClickPendingIntent(R.id.analog_appwidget, pi);
+ }
+
+ wm.updateAppWidget(widgetId, widget);
+ }
}
} \ No newline at end of file
diff --git a/src/com/android/alarmclock/CityAppWidgetProvider.java b/src/com/android/alarmclock/CityAppWidgetProvider.java
new file mode 100644
index 000000000..213aec76f
--- /dev/null
+++ b/src/com/android/alarmclock/CityAppWidgetProvider.java
@@ -0,0 +1,526 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.android.alarmclock;
+
+import android.annotation.SuppressLint;
+import android.app.AlarmManager;
+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.content.res.Resources;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.text.format.DateFormat;
+import android.util.ArraySet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.RemoteViews;
+import android.widget.TextClock;
+import android.widget.TextView;
+
+import com.android.deskclock.DeskClock;
+import com.android.deskclock.LogUtils;
+import com.android.deskclock.R;
+import com.android.deskclock.Utils;
+import com.android.deskclock.data.City;
+import com.android.deskclock.data.DataModel;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Set;
+import java.util.TimeZone;
+
+import static android.app.PendingIntent.FLAG_NO_CREATE;
+import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
+import static android.app.PendingIntent.getBroadcast;
+import static android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE;
+import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_IDS;
+import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT;
+import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH;
+import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT;
+import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH;
+import static android.content.Context.ALARM_SERVICE;
+import static android.content.Intent.ACTION_DATE_CHANGED;
+import static android.content.Intent.ACTION_LOCALE_CHANGED;
+import static android.content.Intent.ACTION_SCREEN_ON;
+import static android.content.Intent.ACTION_TIMEZONE_CHANGED;
+import static android.content.Intent.ACTION_TIME_CHANGED;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.util.TypedValue.COMPLEX_UNIT_PX;
+import static android.view.View.MeasureSpec.UNSPECIFIED;
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+/**
+ * <p>This provider produces a widget resembling:</p>
+ * <pre>
+ * 12:59 AM
+ * ADELAIDE / THU
+ * </pre>
+ *
+ * This widget is scaling the font sizes to fit within the widget bounds chosen by the user without
+ * any clipping. To do so it measures layouts offscreen using a range of font sizes in order to
+ * choose optimal values.
+ */
+public class CityAppWidgetProvider extends AppWidgetProvider {
+
+ private static final LogUtils.Logger LOGGER = new LogUtils.Logger("CityWidgetProvider");
+
+ @Override
+ public void onReceive(@NonNull Context context, @NonNull Intent intent) {
+ LOGGER.i("City Widget processing action %s", intent.getAction());
+ super.onReceive(context, intent);
+
+ final AppWidgetManager wm = AppWidgetManager.getInstance(context);
+ if (wm == null) {
+ return;
+ }
+
+ final ComponentName provider = new ComponentName(context, getClass());
+ final int[] widgetIds = wm.getAppWidgetIds(provider);
+
+ switch (intent.getAction()) {
+ case ACTION_SCREEN_ON:
+ case ACTION_TIME_CHANGED:
+ case ACTION_DATE_CHANGED:
+ case ACTION_LOCALE_CHANGED:
+ case ACTION_TIMEZONE_CHANGED:
+ for (int widgetId : widgetIds) {
+ updateWidget(context, wm, widgetId, wm.getAppWidgetOptions(widgetId));
+ }
+ }
+
+ updateNextDayBroadcast(context, widgetIds);
+
+ final DataModel dm = DataModel.getDataModel();
+ dm.updateWidgetCount(getClass(), widgetIds.length, R.string.category_city_widget);
+ }
+
+ /**
+ * Called when widgets must provide remote views.
+ */
+ @Override
+ public void onUpdate(Context context, AppWidgetManager wm, int[] widgetIds) {
+ super.onUpdate(context, wm, widgetIds);
+
+ for (int widgetId : widgetIds) {
+ updateWidget(context, wm, widgetId, wm.getAppWidgetOptions(widgetId));
+ }
+ }
+
+ /**
+ * Called when a widget changes sizes.
+ */
+ @Override
+ public void onAppWidgetOptionsChanged(Context context, AppWidgetManager wm, int widgetId,
+ Bundle options) {
+ super.onAppWidgetOptionsChanged(context, wm, widgetId, options);
+
+ updateWidget(context, AppWidgetManager.getInstance(context), widgetId, options);
+ }
+
+ /**
+ * Called when widgets have been removed.
+ */
+ @Override
+ public void onDeleted(Context context, int[] widgetIds) {
+ super.onDeleted(context, widgetIds);
+
+ for (int widgetId : widgetIds) {
+ DataModel.getDataModel().setWidgetCity(widgetId, null);
+ }
+ }
+
+ /**
+ * Called when widgets have been restored from backup. Remaps cities associated with old widget
+ * ids to new replacement widget ids.
+ */
+ @Override
+ public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) {
+ super.onRestored(context, oldWidgetIds, newWidgetIds);
+
+ for (int i = 0; i < oldWidgetIds.length; i++) {
+ final int oldWidgetId = oldWidgetIds[i];
+ final int newWidgetId = newWidgetIds[i];
+
+ // Get the city mapped to the old widget id.
+ final City city = DataModel.getDataModel().getWidgetCity(oldWidgetId);
+
+ // Remove the old widget id mapping.
+ DataModel.getDataModel().setWidgetCity(oldWidgetId, null);
+
+ // Create the new widget id mapping.
+ DataModel.getDataModel().setWidgetCity(newWidgetId, city);
+ }
+ }
+
+ /**
+ * Compute optimal font and icon sizes offscreen using the last known widget size and apply them
+ * to the remote views displayed in the widget.
+ */
+ private static void updateWidget(Context context, AppWidgetManager wm, int widgetId,
+ Bundle options) {
+ // Fetch the city to display in this widget.
+ final City city = DataModel.getDataModel().getWidgetCity(widgetId);
+
+ // Return early if there is no city data; occurs while configuration is not yet complete.
+ if (city == null) {
+ return;
+ }
+
+ if (options == null) {
+ options = wm.getAppWidgetOptions(widgetId);
+ }
+
+ // Create a size template that describes the widget bounds.
+ final Resources resources = context.getResources();
+ final float density = resources.getDisplayMetrics().density;
+ final int minWidthPx = (int) (density * options.getInt(OPTION_APPWIDGET_MIN_WIDTH));
+ final int minHeightPx = (int) (density * options.getInt(OPTION_APPWIDGET_MIN_HEIGHT));
+ final int maxWidthPx = (int) (density * options.getInt(OPTION_APPWIDGET_MAX_WIDTH));
+ final int maxHeightPx = (int) (density * options.getInt(OPTION_APPWIDGET_MAX_HEIGHT));
+ final boolean portrait = resources.getConfiguration().orientation == ORIENTATION_PORTRAIT;
+ final int targetWidthPx = portrait ? minWidthPx : maxWidthPx;
+ final int targetHeightPx = portrait ? maxHeightPx : minHeightPx;
+ final int fontSizePx = resources.getDimensionPixelSize(R.dimen.city_widget_name_font_size);
+ final int largestClockFontSizePx =
+ resources.getDimensionPixelSize(R.dimen.city_widget_max_clock_font_size);
+ final Sizes template = new Sizes(city, targetWidthPx, targetHeightPx, fontSizePx,
+ largestClockFontSizePx);
+
+ // Create a remote view for the city widget.
+ final String packageName = context.getPackageName();
+ final RemoteViews widget = new RemoteViews(packageName, R.layout.city_widget);
+
+ // Tapping on the widget opens the app (if not on the lock screen).
+ if (Utils.isWidgetClickable(wm, widgetId)) {
+ final Intent openApp = new Intent(context, DeskClock.class);
+ final PendingIntent pi = PendingIntent.getActivity(context, 0, openApp, 0);
+ widget.setOnClickPendingIntent(R.id.city_widget, pi);
+ }
+
+ // Configure child views of the remote view.
+ widget.setCharSequence(R.id.clock, "setFormat12Hour", get12HourFormat());
+ widget.setCharSequence(R.id.clock, "setFormat24Hour", Utils.get24ModeFormat());
+
+ final CharSequence dateFormat = getDayFormat(context);
+ widget.setCharSequence(R.id.city_day, "setFormat12Hour", dateFormat);
+ widget.setCharSequence(R.id.city_day, "setFormat24Hour", dateFormat);
+
+ widget.setTextViewText(R.id.city_name, template.getCityName());
+ widget.setString(R.id.clock, "setTimeZone", template.getTimeZoneId());
+ widget.setString(R.id.city_day, "setTimeZone", template.getTimeZoneId());
+
+ // Compute optimal font sizes to fit within the widget bounds.
+ final Sizes sizes = optimizeSizes(context, template);
+ if (LOGGER.isVerboseLoggable()) {
+ LOGGER.v(sizes.toString());
+ }
+
+ // Apply the computed sizes to the remote views.
+ widget.setTextViewTextSize(R.id.clock, COMPLEX_UNIT_PX, sizes.mClockFontSizePx);
+ widget.setTextViewTextSize(R.id.city_day, COMPLEX_UNIT_PX, sizes.mFontSizePx);
+ widget.setTextViewTextSize(R.id.city_name, COMPLEX_UNIT_PX, sizes.mFontSizePx);
+ widget.setInt(R.id.city_name, "setMaxWidth", sizes.mCityNameMaxWidthPx);
+ wm.updateAppWidget(widgetId, widget);
+ }
+
+ /**
+ * Inflate an offscreen copy of the widget views. Binary search through the range of font sizes
+ * until the optimal font sizes that fit within the widget bounds are located.
+ */
+ private static Sizes optimizeSizes(Context context, Sizes template) {
+ // Inflate a test layout to compute sizes at different font sizes.
+ final LayoutInflater inflater = LayoutInflater.from(context);
+ @SuppressLint("InflateParams")
+ final View sizer = inflater.inflate(R.layout.city_widget, null /* root */);
+
+ // Configure the clock to display the preferred time formats.
+ final TextClock clock = (TextClock) sizer.findViewById(R.id.clock);
+ clock.setFormat12Hour(get12HourFormat());
+ clock.setFormat24Hour(Utils.get24ModeFormat());
+
+ // Configure the date to display the preferred day format.
+ final CharSequence dateFormat = getDayFormat(context);
+ final TextClock cityDay = (TextClock) sizer.findViewById(R.id.city_day);
+ cityDay.setFormat12Hour(dateFormat);
+ cityDay.setFormat24Hour(dateFormat);
+
+ // Measure the widget at the largest possible size.
+ Sizes high = measure(template, template.getLargestClockFontSizePx(), sizer);
+ if (!high.hasViolations()) {
+ return high;
+ }
+
+ // Measure the widget at the smallest possible size.
+ Sizes low = measure(template, template.getSmallestClockFontSizePx(), sizer);
+ if (low.hasViolations()) {
+ return low;
+ }
+
+ // Binary search between the smallest and largest sizes until an optimum size is found.
+ while (low.getClockFontSizePx() != high.getClockFontSizePx()) {
+ final int midFontSize = (low.getClockFontSizePx() + high.getClockFontSizePx()) / 2;
+ if (midFontSize == low.getClockFontSizePx()) {
+ return low;
+ }
+
+ final Sizes midSize = measure(template, midFontSize, sizer);
+ if (midSize.hasViolations()) {
+ high = midSize;
+ } else {
+ low = midSize;
+ }
+ }
+
+ return low;
+ }
+
+ /**
+ * Compute all font sizes based on the given {@code clockFontSizePx} and apply them to the
+ * offscreen {@code sizer} view. Measure the {@code sizer} view and return the resulting size
+ * measurements. Since the localized strings for "AM" and "PM" may be different, layouts for
+ * morning and afternoon times are measured and the largest dimensions are reported as the
+ * required size.
+ */
+ private static Sizes measure(Sizes template, int clockFontSizePx, View sizer) {
+ final TextClock clock = (TextClock) sizer.findViewById(R.id.clock);
+ clock.setTimeZone(template.getTimeZoneId());
+
+ final CharSequence amTime = getLongestAMTimeString(clock);
+ final CharSequence pmTime = getLongestPMTimeString(clock);
+
+ final TextClock cityDay = (TextClock) sizer.findViewById(R.id.city_day);
+ cityDay.setTimeZone(template.getTimeZoneId());
+ // The city name will be elided to fit in its bounds. Don't set a city name which could
+ // trigger a false layout violation.
+
+ // Measure the size of the widget at 11:59AM and 11:59PM.
+ final Sizes amSizes = measure(template, clockFontSizePx, amTime, sizer);
+ final Sizes pmSizes = measure(template, clockFontSizePx, pmTime, sizer);
+
+ // Report the largest dimensions between the two different times.
+ final Sizes merged = amSizes.newSize();
+ merged.setClockFontSizePx(clockFontSizePx);
+ merged.mMeasuredWidthPx = max(amSizes.mMeasuredWidthPx, pmSizes.mMeasuredWidthPx);
+ merged.mMeasuredHeightPx = max(amSizes.mMeasuredHeightPx, pmSizes.mMeasuredHeightPx);
+ merged.mMeasuredTextClockWidthPx =
+ max(amSizes.mMeasuredTextClockWidthPx, pmSizes.mMeasuredTextClockWidthPx);
+ merged.mMeasuredTextClockHeightPx =
+ max(amSizes.mMeasuredTextClockHeightPx, pmSizes.mMeasuredTextClockHeightPx);
+ merged.mCityNameMaxWidthPx = min(amSizes.mCityNameMaxWidthPx, pmSizes.mCityNameMaxWidthPx);
+ return merged;
+ }
+
+ /**
+ * Compute all font and icon sizes based on the given {@code clockFontSizePx} at the given
+ * {@code time} and apply them to the offscreen {@code sizer} view. Measure the {@code sizer}
+ * view and return the resulting size measurements.
+ */
+ private static Sizes measure(Sizes template, int clockFontSizePx, CharSequence time, View sizer) {
+ // Create a copy of the given template sizes.
+ final Sizes measuredSizes = template.newSize();
+
+ // Configure the clock to display the time string.
+ final TextClock clock = (TextClock) sizer.findViewById(R.id.clock);
+ final TextClock cityDay = (TextClock) sizer.findViewById(R.id.city_day);
+ final TextView cityName = (TextView) sizer.findViewById(R.id.city_name);
+
+ // Adjust the font sizes.
+ measuredSizes.setClockFontSizePx(clockFontSizePx);
+ clock.setText(time);
+ clock.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mClockFontSizePx);
+ cityDay.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mFontSizePx);
+ cityName.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mFontSizePx);
+
+ // Measure and layout the sizer.
+ final int widthSize = View.MeasureSpec.getSize(measuredSizes.mTargetWidthPx);
+ final int heightSize = View.MeasureSpec.getSize(measuredSizes.mTargetHeightPx);
+ final int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(widthSize, UNSPECIFIED);
+ final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(heightSize, UNSPECIFIED);
+ sizer.measure(widthMeasureSpec, heightMeasureSpec);
+ sizer.layout(0, 0, sizer.getMeasuredWidth(), sizer.getMeasuredHeight());
+
+ // Copy the measurements into the result object.
+ measuredSizes.mMeasuredWidthPx = sizer.getMeasuredWidth();
+ measuredSizes.mMeasuredHeightPx = sizer.getMeasuredHeight();
+ measuredSizes.mMeasuredTextClockWidthPx = clock.getMeasuredWidth();
+ measuredSizes.mMeasuredTextClockHeightPx = clock.getMeasuredHeight();
+ measuredSizes.mCityNameMaxWidthPx = template.mTargetWidthPx - cityDay.getMeasuredWidth();
+
+ return measuredSizes;
+ }
+
+ /**
+ * @return "11:59AM" or "11:59" in the current locale
+ */
+ private static CharSequence getLongestAMTimeString(TextClock clock) {
+ final CharSequence format = clock.is24HourModeEnabled()
+ ? clock.getFormat24Hour()
+ : clock.getFormat12Hour();
+ final Calendar longestAMTime = Calendar.getInstance();
+ longestAMTime.set(0, 0, 0, 11, 59);
+ return DateFormat.format(format, longestAMTime);
+ }
+
+ /**
+ * @return "11:59PM" or "23:59" in the current locale
+ */
+ private static CharSequence getLongestPMTimeString(TextClock clock) {
+ final CharSequence format = clock.is24HourModeEnabled()
+ ? clock.getFormat24Hour()
+ : clock.getFormat12Hour();
+ final Calendar longestPMTime = Calendar.getInstance();
+ longestPMTime.set(0, 0, 0, 23, 59);
+ return DateFormat.format(format, longestPMTime);
+ }
+
+ /**
+ * @return the locale-specific date pattern
+ */
+ private static String getDayFormat(Context context) {
+ return context.getString(R.string.abbrev_wday);
+ }
+
+ /**
+ * @return the locale-specific 12-hour time format with the AM/PM string scaled to 40% of the
+ * normal font height
+ */
+ private static CharSequence get12HourFormat() {
+ return Utils.get12ModeFormat(0.4f /* amPmRatio */);
+ }
+
+ /**
+ * Schedule or cancel the next-day broadcast as necessary. This broadcast is necessary because
+ * the week day displayed in the city widget can vary in width for some locales and thus a
+ * layout refresh must be computed.
+ */
+ private static void updateNextDayBroadcast(Context context, int[] widgetIds) {
+ // Fetch all time zones represented by all city widgets.
+ final Set<TimeZone> zones = new ArraySet<>(widgetIds.length);
+ for (int widgetId : widgetIds) {
+ final City city = DataModel.getDataModel().getWidgetCity(widgetId);
+ if (city != null) {
+ zones.add(city.getTimeZone());
+ }
+ }
+
+ // Build an intent that will update all city widgets.
+ final Intent update =
+ new Intent(ACTION_APPWIDGET_UPDATE, null, context, CityAppWidgetProvider.class)
+ .putExtra(EXTRA_APPWIDGET_IDS, widgetIds);
+ final AlarmManager am = (AlarmManager) context.getSystemService(ALARM_SERVICE);
+
+ if (zones.isEmpty()) {
+ // No city widgets exist so cancel the next-day broadcast.
+ final PendingIntent pi = getBroadcast(context, 0, update, FLAG_NO_CREATE);
+ if (pi != null) {
+ am.cancel(pi);
+ pi.cancel();
+ }
+ } else {
+ // Compute the next time at which the day changes in any of the time zones.
+ final Date nextMidnight = Utils.getNextDay(new Date(), zones);
+
+ // Schedule an intent to update all city widgets on next day change.
+ final PendingIntent pi = getBroadcast(context, 0, update, FLAG_UPDATE_CURRENT);
+ am.setExact(AlarmManager.RTC, nextMidnight.getTime(), pi);
+ }
+ }
+
+ /**
+ * This class stores the target size of the widget as well as the measured size using a given
+ * clock font size. All other fonts and icons are scaled proportional to the clock font.
+ */
+ private static final class Sizes {
+
+ private final City mCity;
+ private final int mTargetWidthPx;
+ private final int mTargetHeightPx;
+ private final int mLargestClockFontSizePx;
+ private final int mSmallestClockFontSizePx;
+
+ private int mMeasuredWidthPx;
+ private int mMeasuredHeightPx;
+ private int mMeasuredTextClockWidthPx;
+ private int mMeasuredTextClockHeightPx;
+
+ /** The size of the font to use on the city name / day of week fields. */
+ private final int mFontSizePx;
+
+ /** The size of the font to use on the clock field. */
+ private int mClockFontSizePx;
+
+ /** If the city name requires more width that this threshold the text is elided. */
+ private int mCityNameMaxWidthPx;
+
+ private Sizes(City city, int targetWidthPx, int targetHeightPx, int fontSizePx,
+ int largestClockFontSizePx) {
+ mCity = city;
+ mTargetWidthPx = targetWidthPx;
+ mTargetHeightPx = targetHeightPx;
+ mFontSizePx = fontSizePx;
+ mLargestClockFontSizePx = largestClockFontSizePx;
+ mSmallestClockFontSizePx = 0;
+ }
+
+ private String getCityName() { return mCity.getName(); }
+ private String getTimeZoneId() { return mCity.getTimeZone().getID(); }
+ private int getClockFontSizePx() { return mClockFontSizePx; }
+ private void setClockFontSizePx(int clockFontSizePx) { mClockFontSizePx = clockFontSizePx; }
+ private int getLargestClockFontSizePx() { return mLargestClockFontSizePx; }
+ private int getSmallestClockFontSizePx() { return mSmallestClockFontSizePx; }
+
+ private boolean hasViolations() {
+ return mMeasuredWidthPx > mTargetWidthPx || mMeasuredHeightPx > mTargetHeightPx;
+ }
+
+ private Sizes newSize() {
+ return new Sizes(mCity, mTargetWidthPx, mTargetHeightPx, mFontSizePx,
+ mLargestClockFontSizePx);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder(1000);
+ builder.append("\n");
+ append(builder, "Target dimensions: %dpx x %dpx\n", mTargetWidthPx, mTargetHeightPx);
+ append(builder, "Last valid widget container measurement: %dpx x %dpx\n",
+ mMeasuredWidthPx, mMeasuredHeightPx);
+ append(builder, "Last text clock measurement: %dpx x %dpx\n",
+ mMeasuredTextClockWidthPx, mMeasuredTextClockHeightPx);
+ if (mMeasuredWidthPx > mTargetWidthPx) {
+ append(builder, "Measured width %dpx exceeded widget width %dpx\n",
+ mMeasuredWidthPx, mTargetWidthPx);
+ }
+ if (mMeasuredHeightPx > mTargetHeightPx) {
+ append(builder, "Measured height %dpx exceeded widget height %dpx\n",
+ mMeasuredHeightPx, mTargetHeightPx);
+ }
+ append(builder, "Clock font: %dpx\n", mClockFontSizePx);
+ return builder.toString();
+ }
+
+ private static void append(StringBuilder builder, String format, Object... args) {
+ builder.append(String.format(Locale.ENGLISH, format, args));
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/alarmclock/DigitalAppWidgetProvider.java b/src/com/android/alarmclock/DigitalAppWidgetProvider.java
index 57c349745..76c1c903c 100644
--- a/src/com/android/alarmclock/DigitalAppWidgetProvider.java
+++ b/src/com/android/alarmclock/DigitalAppWidgetProvider.java
@@ -36,9 +36,11 @@ import android.widget.RemoteViews;
import android.widget.TextClock;
import android.widget.TextView;
+import com.android.deskclock.DeskClock;
import com.android.deskclock.LogUtils;
import com.android.deskclock.R;
import com.android.deskclock.Utils;
+import com.android.deskclock.data.DataModel;
import java.util.Calendar;
import java.util.Locale;
@@ -57,8 +59,6 @@ import static android.util.TypedValue.COMPLEX_UNIT_PX;
import static android.view.View.GONE;
import static android.view.View.MeasureSpec.UNSPECIFIED;
import static android.view.View.VISIBLE;
-import static com.android.deskclock.HandleDeskClockApiCalls.ACTION_SHOW_CLOCK;
-import static com.android.deskclock.HandleDeskClockApiCalls.EXTRA_EVENT_LABEL;
import static com.android.deskclock.alarms.AlarmStateManager.SYSTEM_ALARM_CHANGE_ACTION;
import static java.lang.Math.max;
@@ -77,18 +77,14 @@ import static java.lang.Math.max;
* WED, FEB 3
* </pre>
*
- * This widget is scaling the font sizes up to fit within the widget bounds chosen by the user
- * without any clipping. To do so it measures layouts offscreen using a range of font sizes in order
- * to choose optimal values.
+ * This widget is scaling the font sizes to fit within the widget bounds chosen by the user without
+ * any clipping. To do so it measures layouts offscreen using a range of font sizes in order to
+ * choose optimal values.
*/
public class DigitalAppWidgetProvider extends AppWidgetProvider {
private static final LogUtils.Logger LOGGER = new LogUtils.Logger("DigitalWidgetProvider");
- /** Intent used to open the application when tapping on the widget. */
- private static final Intent SHOW_CLOCK_INTENT = new Intent(ACTION_SHOW_CLOCK)
- .putExtra(EXTRA_EVENT_LABEL, R.string.label_widget);
-
/** A custom font containing a special glyph that draws a clock icon with proper drop shadow. */
private static Typeface sAlarmIconTypeface;
@@ -102,6 +98,9 @@ public class DigitalAppWidgetProvider extends AppWidgetProvider {
return;
}
+ final ComponentName provider = new ComponentName(context, getClass());
+ final int[] widgetIds = wm.getAppWidgetIds(provider);
+
switch (intent.getAction()) {
case ACTION_SCREEN_ON:
case ACTION_DATE_CHANGED:
@@ -109,24 +108,24 @@ public class DigitalAppWidgetProvider extends AppWidgetProvider {
case ACTION_TIMEZONE_CHANGED:
case SYSTEM_ALARM_CHANGE_ACTION:
case ACTION_NEXT_ALARM_CLOCK_CHANGED:
- final ComponentName provider = new ComponentName(context, getClass());
- final int[] widgetIds = wm.getAppWidgetIds(provider);
-
for (int widgetId : widgetIds) {
- updateRemoteViews(context, wm, widgetId, wm.getAppWidgetOptions(widgetId));
+ updateWidget(context, wm, widgetId, wm.getAppWidgetOptions(widgetId));
}
}
+
+ final DataModel dm = DataModel.getDataModel();
+ dm.updateWidgetCount(getClass(), widgetIds.length, R.string.category_digital_widget);
}
/**
- * Called when this widget must provide remote views.
+ * Called when widgets must provide remote views.
*/
@Override
public void onUpdate(Context context, AppWidgetManager wm, int[] widgetIds) {
super.onUpdate(context, wm, widgetIds);
for (int widgetId : widgetIds) {
- updateRemoteViews(context, wm, widgetId, wm.getAppWidgetOptions(widgetId));
+ updateWidget(context, wm, widgetId, wm.getAppWidgetOptions(widgetId));
}
}
@@ -139,14 +138,14 @@ public class DigitalAppWidgetProvider extends AppWidgetProvider {
super.onAppWidgetOptionsChanged(context, wm, widgetId, options);
// scale the fonts of the clock to fit inside the new size
- updateRemoteViews(context, AppWidgetManager.getInstance(context), widgetId, options);
+ updateWidget(context, AppWidgetManager.getInstance(context), widgetId, options);
}
/**
* Compute optimal font and icon sizes offscreen using the last known widget size and apply them
- * to the remote views displayed in the widget.
+ * to the widget.
*/
- private static void updateRemoteViews(Context context, AppWidgetManager wm, int widgetId,
+ private static void updateWidget(Context context, AppWidgetManager wm, int widgetId,
Bundle options) {
// Create a remote view for the digital clock.
final String packageName = context.getPackageName();
@@ -154,7 +153,8 @@ public class DigitalAppWidgetProvider extends AppWidgetProvider {
// Tapping on the widget opens the app (if not on the lock screen).
if (Utils.isWidgetClickable(wm, widgetId)) {
- final PendingIntent pi = PendingIntent.getActivity(context, 0, SHOW_CLOCK_INTENT, 0);
+ final Intent openApp = new Intent(context, DeskClock.class);
+ final PendingIntent pi = PendingIntent.getActivity(context, 0, openApp, 0);
widget.setOnClickPendingIntent(R.id.digital_widget, pi);
}
@@ -177,7 +177,7 @@ public class DigitalAppWidgetProvider extends AppWidgetProvider {
}
if (options == null) {
- options = AppWidgetManager.getInstance(context).getAppWidgetOptions(widgetId);
+ options = wm.getAppWidgetOptions(widgetId);
}
// Fetch the widget size selected by the user.
diff --git a/src/com/android/deskclock/DeskClockApplication.java b/src/com/android/deskclock/DeskClockApplication.java
index fa574dc2b..b88448ff2 100644
--- a/src/com/android/deskclock/DeskClockApplication.java
+++ b/src/com/android/deskclock/DeskClockApplication.java
@@ -23,6 +23,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
+import com.android.alarmclock.CityAppWidgetProvider;
import com.android.alarmclock.DigitalAppWidgetProvider;
import com.android.deskclock.data.DataModel;
import com.android.deskclock.events.Events;
@@ -53,6 +54,7 @@ public class DeskClockApplication extends Application {
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
+ updateWidgets(CityAppWidgetProvider.class);
updateWidgets(DigitalAppWidgetProvider.class);
}
diff --git a/src/com/android/deskclock/Utils.java b/src/com/android/deskclock/Utils.java
index 7eb69c740..9ff74b42c 100644
--- a/src/com/android/deskclock/Utils.java
+++ b/src/com/android/deskclock/Utils.java
@@ -425,6 +425,38 @@ public class Utils {
}
/**
+ * Given a point in time, return the subsequent moment any of the time zones changes days.
+ * e.g. Given 8:00pm on 1/1/2016 and time zones in LA and NY this method would return a Date for
+ * midnight on 1/2/2016 in the NY timezone since it changes days first.
+ *
+ * @param time a point in time from which to compute midnight on the subsequent day
+ * @param zones a collection of time zones
+ * @return the nearest point in the future at which any of the time zones changes days
+ */
+ public static Date getNextDay(Date time, Collection<TimeZone> zones) {
+ Calendar next = null;
+ for (TimeZone tz : zones) {
+ final Calendar c = Calendar.getInstance(tz);
+ c.setTime(time);
+
+ // Advance to the next day.
+ c.add(Calendar.DAY_OF_YEAR, 1);
+
+ // Reset the time to midnight.
+ c.set(Calendar.HOUR_OF_DAY, 0);
+ c.set(Calendar.MINUTE, 0);
+ c.set(Calendar.SECOND, 0);
+ c.set(Calendar.MILLISECOND, 0);
+
+ if (next == null || c.compareTo(next) < 0) {
+ next = c;
+ }
+ }
+
+ return next == null ? null : next.getTime();
+ }
+
+ /**
* Convenience method for retrieving a themed color value.
*
* @param context the {@link Context} to resolve the theme attribute against
diff --git a/src/com/android/deskclock/data/CityModel.java b/src/com/android/deskclock/data/CityModel.java
index 0b345e74d..195189fe7 100644
--- a/src/com/android/deskclock/data/CityModel.java
+++ b/src/com/android/deskclock/data/CityModel.java
@@ -217,6 +217,14 @@ final class CityModel {
mUnselectedCities = null;
}
+ /**
+ * @param cityId identifies a city
+ * @return the corresponding city
+ */
+ City getCityById(String cityId) {
+ return getCityMap().get(cityId);
+ }
+
private Map<String, City> getCityMap() {
if (mCityMap == null) {
mCityMap = CityDAO.getCities(mContext);
diff --git a/src/com/android/deskclock/data/DataModel.java b/src/com/android/deskclock/data/DataModel.java
index ff98c4dff..ad0e226bc 100644
--- a/src/com/android/deskclock/data/DataModel.java
+++ b/src/com/android/deskclock/data/DataModel.java
@@ -59,6 +59,9 @@ public final class DataModel {
/** The model from which alarm data are fetched. */
private AlarmModel mAlarmModel;
+ /** The model from which widget data are fetched. */
+ private WidgetModel mWidgetModel;
+
/** The model from which stopwatch data are fetched. */
private StopwatchModel mStopwatchModel;
@@ -83,6 +86,7 @@ public final class DataModel {
mSettingsModel = new SettingsModel(mContext);
mNotificationModel = new NotificationModel();
mCityModel = new CityModel(mContext, mSettingsModel);
+ mWidgetModel = new WidgetModel(mContext, mCityModel);
mAlarmModel = new AlarmModel(mContext, mSettingsModel);
mStopwatchModel = new StopwatchModel(mContext, mNotificationModel);
mTimerModel = new TimerModel(mContext, mSettingsModel, mNotificationModel);
@@ -589,6 +593,38 @@ public final class DataModel {
}
//
+ // Widgets
+ //
+
+ /**
+ * @param widgetId identifies a city widget in the launcher
+ * @return the City data to display in the widget
+ */
+ public City getWidgetCity(int widgetId) {
+ enforceMainLooper();
+ return mWidgetModel.getWidgetCity(widgetId);
+ }
+
+ /**
+ * @param widgetId identifies a city widget in the launcher
+ * @param city the City to display in the widget; {@code null} implies remove City
+ */
+ public void setWidgetCity(int widgetId, City city) {
+ enforceMainLooper();
+ mWidgetModel.setWidgetCity(widgetId, city);
+ }
+
+ /**
+ * @param widgetClass indicates the type of widget being counted
+ * @param count the number of widgets of the given type
+ * @param eventCategoryId identifies the category of event to send
+ */
+ public void updateWidgetCount(Class widgetClass, int count, @StringRes int eventCategoryId) {
+ enforceMainLooper();
+ mWidgetModel.updateWidgetCount(widgetClass, count, eventCategoryId);
+ }
+
+ //
// Settings
//
diff --git a/src/com/android/deskclock/data/WidgetDAO.java b/src/com/android/deskclock/data/WidgetDAO.java
new file mode 100644
index 000000000..7068f50c6
--- /dev/null
+++ b/src/com/android/deskclock/data/WidgetDAO.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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.android.deskclock.data;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+/**
+ * This class encapsulates the transfer of data between widget objects and their permanent storage
+ * in {@link SharedPreferences}.
+ */
+final class WidgetDAO {
+
+ /** Prefix for a key to a preference that stores the id of a city displayed in a widget. */
+ private static final String WIDGET_CITY_ID_PREFIX = "widget_city_id_";
+
+ /** Suffix for a key to a preference that stores the instance count for a given widget type. */
+ private static final String WIDGET_COUNT_SUFFIX = "_widget_count";
+
+ /** Lazily instantiated and cached for the life of the application. */
+ private static SharedPreferences sPrefs;
+
+ private WidgetDAO() {}
+
+ /**
+ * @param widgetId identifies a city widget in the launcher
+ * @return the id of the City to display in the widget
+ */
+ public static String getWidgetCityId(Context context, int widgetId) {
+ final SharedPreferences prefs = getSharedPreferences(context);
+ return prefs.getString(WIDGET_CITY_ID_PREFIX + widgetId, null);
+ }
+
+ /**
+ * @param widgetId identifies a city widget in the launcher
+ * @param cityId identifies the City to display in the widget; {@code null} implies remove City
+ */
+ public static void setWidgetCityId(Context context, int widgetId, String cityId) {
+ final SharedPreferences prefs = getSharedPreferences(context);
+ if (cityId == null) {
+ prefs.edit().remove(WIDGET_CITY_ID_PREFIX + widgetId).apply();
+ } else {
+ prefs.edit().putString(WIDGET_CITY_ID_PREFIX + widgetId, cityId).apply();
+ }
+ }
+
+ /**
+ * @param widgetProviderClass indicates the type of widget being counted
+ * @param count the number of widgets of the given type
+ * @return the delta between the new count and the old count
+ */
+ public static int updateWidgetCount(Context context, Class widgetProviderClass, int count) {
+ final SharedPreferences prefs = getSharedPreferences(context);
+ final String key = widgetProviderClass.getSimpleName() + WIDGET_COUNT_SUFFIX;
+ final int oldCount = prefs.getInt(key, 0);
+ if (count == 0) {
+ prefs.edit().remove(key).apply();
+ } else {
+ prefs.edit().putInt(key, count).apply();
+ }
+ return count - oldCount;
+ }
+
+ private static SharedPreferences getSharedPreferences(Context context) {
+ if (sPrefs == null) {
+ sPrefs = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
+ }
+
+ return sPrefs;
+ }
+} \ No newline at end of file
diff --git a/src/com/android/deskclock/data/WidgetModel.java b/src/com/android/deskclock/data/WidgetModel.java
new file mode 100644
index 000000000..6ccd1f4cb
--- /dev/null
+++ b/src/com/android/deskclock/data/WidgetModel.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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.android.deskclock.data;
+
+import android.content.Context;
+import android.support.annotation.StringRes;
+import android.util.ArrayMap;
+
+import com.android.deskclock.R;
+import com.android.deskclock.events.Events;
+
+import java.util.Map;
+
+/**
+ * All widget data is accessed via this model.
+ */
+final class WidgetModel {
+
+ private final Context mContext;
+
+ /** The model from which city data are fetched. */
+ private final CityModel mCityModel;
+
+ /** Maps widget ID to city ID; items are loaded individually as widgets request data. */
+ private final Map<Integer, String> mWidgetCityMap = new ArrayMap<>();
+
+ WidgetModel(Context context, CityModel cityModel) {
+ mContext = context;
+ mCityModel = cityModel;
+ }
+
+ /**
+ * @param widgetId identifies a city widget in the launcher
+ * @return the City data to display in the widget
+ */
+ City getWidgetCity(int widgetId) {
+ String cityId = mWidgetCityMap.get(widgetId);
+ if (cityId == null) {
+ cityId = WidgetDAO.getWidgetCityId(mContext, widgetId);
+ mWidgetCityMap.put(widgetId, cityId);
+ }
+
+ return mCityModel.getCityById(cityId);
+ }
+
+ /**
+ * @param widgetId identifies a city widget in the launcher
+ * @param city the City to display in the widget; {@code null} implies remove City
+ */
+ void setWidgetCity(int widgetId, City city) {
+ final String cityId = city == null ? null : city.getId();
+ WidgetDAO.setWidgetCityId(mContext, widgetId, cityId);
+ if (cityId == null) {
+ mWidgetCityMap.remove(widgetId);
+ } else {
+ mWidgetCityMap.put(widgetId, cityId);
+ }
+ }
+
+ /**
+ * @param widgetClass indicates the type of widget being counted
+ * @param count the number of widgets of the given type
+ * @param eventCategoryId identifies the category of event to send
+ */
+ void updateWidgetCount(Class widgetClass, int count, @StringRes int eventCategoryId) {
+ int delta = WidgetDAO.updateWidgetCount(mContext, widgetClass, count);
+ for (; delta > 0; delta--) {
+ Events.sendEvent(eventCategoryId, R.string.action_create, 0);
+ }
+ for (; delta < 0; delta++) {
+ Events.sendEvent(eventCategoryId, R.string.action_delete, 0);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/deskclock/worldclock/CitySelectionActivity.java b/src/com/android/deskclock/worldclock/CitySelectionActivity.java
index 1bba2cb7e..9a09d2d66 100644
--- a/src/com/android/deskclock/worldclock/CitySelectionActivity.java
+++ b/src/com/android/deskclock/worldclock/CitySelectionActivity.java
@@ -296,7 +296,7 @@ public final class CitySelectionActivity extends BaseActivity {
}
@Override
- public synchronized View getView(int position, View view, ViewGroup parent) {
+ public View getView(int position, View view, ViewGroup parent) {
final int itemViewType = getItemViewType(position);
switch (itemViewType) {
case VIEW_TYPE_SELECTED_CITIES_HEADER:
diff --git a/src/com/android/deskclock/worldclock/WidgetCitySelectionActivity.java b/src/com/android/deskclock/worldclock/WidgetCitySelectionActivity.java
new file mode 100644
index 000000000..a9e548214
--- /dev/null
+++ b/src/com/android/deskclock/worldclock/WidgetCitySelectionActivity.java
@@ -0,0 +1,439 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.android.deskclock.worldclock;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.widget.SearchView;
+import android.text.TextUtils;
+import android.text.format.DateFormat;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.SectionIndexer;
+import android.widget.TextView;
+
+import com.android.alarmclock.CityAppWidgetProvider;
+import com.android.deskclock.BaseActivity;
+import com.android.deskclock.DropShadowController;
+import com.android.deskclock.R;
+import com.android.deskclock.actionbarmenu.ActionBarMenuManager;
+import com.android.deskclock.actionbarmenu.NavUpMenuItemController;
+import com.android.deskclock.actionbarmenu.SearchMenuItemController;
+import com.android.deskclock.data.City;
+import com.android.deskclock.data.DataModel;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import static android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE;
+import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID;
+import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_IDS;
+import static android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID;
+
+/**
+ * This activity allows the user to select a single city for display in a widget.
+ */
+public final class WidgetCitySelectionActivity extends BaseActivity {
+
+ /** Manages all action bar menu display and click handling. */
+ private final ActionBarMenuManager mActionBarMenuManager = new ActionBarMenuManager();
+
+ /** The list of all cities, indexed and possibly filtered. */
+ private ListView mCitiesList;
+
+ /** The adapter that presents all of the cities. */
+ private CityAdapter mCitiesAdapter;
+
+ /** Menu item controller for search view. */
+ private SearchMenuItemController mSearchMenuItemController;
+
+ /** The controller that shows the drop shadow when content is not scrolled to the top. */
+ private DropShadowController mDropShadowController;
+
+ /** Identifies the widget in which the selected city will be displayed. */
+ private int mWidgetId = INVALID_APPWIDGET_ID;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Set the result to CANCELED. This will cause the widget host to cancel
+ // out of the widget placement if they press the back button.
+ setResult(RESULT_CANCELED);
+
+ final Bundle extras = getIntent().getExtras();
+ if (extras != null) {
+ mWidgetId = extras.getInt(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID);
+ }
+
+ setContentView(R.layout.cities_activity);
+ mSearchMenuItemController =
+ new SearchMenuItemController(new SearchView.OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ return false;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String query) {
+ mCitiesAdapter.filter(query);
+ updateFastScrolling();
+ return true;
+ }
+ }, savedInstanceState);
+ mCitiesAdapter = new CityAdapter(this);
+ mActionBarMenuManager.addMenuItemController(new NavUpMenuItemController(this))
+ .addMenuItemController(mSearchMenuItemController);
+ mCitiesList = (ListView) findViewById(R.id.cities_list);
+ mCitiesList.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET);
+ mCitiesList.setAdapter(mCitiesAdapter);
+
+ updateFastScrolling();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle bundle) {
+ super.onSaveInstanceState(bundle);
+ mSearchMenuItemController.saveInstance(bundle);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ // Recompute the contents of the adapter before displaying on screen.
+ mCitiesAdapter.refresh();
+
+ final View dropShadow = findViewById(R.id.drop_shadow);
+ mDropShadowController = new DropShadowController(dropShadow, mCitiesList);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+
+ mDropShadowController.stop();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ mActionBarMenuManager.createOptionsMenu(menu, getMenuInflater());
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ mActionBarMenuManager.prepareShowMenu(menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (mActionBarMenuManager.handleMenuItemClick(item)) {
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * Fast scrolling is only enabled while no filtering is happening.
+ */
+ private void updateFastScrolling() {
+ final boolean enabled = !mCitiesAdapter.isFiltering();
+ mCitiesList.setFastScrollAlwaysVisible(enabled);
+ mCitiesList.setFastScrollEnabled(enabled);
+ }
+
+ /**
+ * This adapter presents data like so:
+ *
+ * <pre>
+ * A City A1 (alphabetically first starting with A)
+ * City A2 (alphabetically second starting with A)
+ * ...
+ * B City B1 (alphabetically first starting with B)
+ * City B2 (alphabetically second starting with B)
+ * ...
+ * </pre>
+ */
+ private final class CityAdapter extends BaseAdapter implements View.OnClickListener,
+ SectionIndexer {
+
+ private final Context mContext;
+
+ private final LayoutInflater mInflater;
+
+ /** Orders the cities by name for presentation in the UI. */
+ private final Comparator<City> mNameIndexComparator = new City.NameIndexComparator();
+
+ /** The 12-hour time pattern for the current locale. */
+ private final String mPattern12;
+
+ /** The 24-hour time pattern for the current locale. */
+ private final String mPattern24;
+
+ /** {@code true} time should honor {@link #mPattern24}; {@link #mPattern12} otherwise. */
+ private boolean mIs24HoursMode;
+
+ /** A calendar used to format time in a particular timezone. */
+ private final Calendar mCalendar;
+
+ /** The list of cities which may be filtered by a search term. */
+ private List<City> mFilteredCities = Collections.emptyList();
+
+ /** The precomputed section headers. */
+ private String[] mSectionHeaders;
+
+ /** The corresponding location of each precomputed section header. */
+ private Integer[] mSectionHeaderPositions;
+
+ public CityAdapter(Context context) {
+ mContext = context;
+ mInflater = LayoutInflater.from(context);
+
+ mCalendar = Calendar.getInstance();
+ mCalendar.setTimeInMillis(System.currentTimeMillis());
+
+ final Locale locale = Locale.getDefault();
+ mPattern24 = DateFormat.getBestDateTimePattern(locale, "Hm");
+
+ String pattern12 = DateFormat.getBestDateTimePattern(locale, "hma");
+ if (TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL) {
+ // There's an RTL layout bug that causes jank when fast-scrolling through
+ // the list in 12-hour mode in an RTL locale. We can work around this by
+ // ensuring the strings are the same length by using "hh" instead of "h".
+ pattern12 = pattern12.replaceAll("h", "hh");
+ }
+ mPattern12 = pattern12;
+ }
+
+ @Override
+ public int getCount() {
+ return mFilteredCities.size();
+ }
+
+ @Override
+ public City getItem(int position) {
+ return mFilteredCities.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ final City city = getItem(position);
+ final TimeZone timeZone = city.getTimeZone();
+
+ // Inflate a new view if necessary.
+ if (view == null) {
+ view = mInflater.inflate(R.layout.widget_city_list_item, parent, false);
+ final TextView index = (TextView) view.findViewById(R.id.index);
+ final TextView name = (TextView) view.findViewById(R.id.city_name);
+ final TextView time = (TextView) view.findViewById(R.id.city_time);
+ view.setTag(new CityItemHolder(index, name, time));
+ }
+
+ // Bind data into the child views.
+ final CityItemHolder holder = (CityItemHolder) view.getTag();
+ holder.name.setText(city.getName(), TextView.BufferType.SPANNABLE);
+ holder.time.setText(getTimeCharSequence(timeZone));
+
+ final boolean showIndex = getShowIndex(position);
+ holder.index.setVisibility(showIndex ? View.VISIBLE : View.INVISIBLE);
+ if (showIndex) {
+ holder.index.setText(city.getIndexString());
+ holder.index.setTextSize(TypedValue.COMPLEX_UNIT_SP, 24);
+ }
+
+ // skip checkbox and other animations
+ view.jumpDrawablesToCurrentState();
+ view.setOnClickListener(this);
+ return view;
+ }
+
+ @Override
+ public void onClick(View v) {
+ final int position = mCitiesList.getPositionForView(v);
+ if (position >= 0) {
+ final City selectedCity = getItem(position);
+
+ // Associate the widget id to the selected city.
+ DataModel.getDataModel().setWidgetCity(mWidgetId, selectedCity);
+
+ // Broadcast an intent to update the instance of the widget now that data exists.
+ final Intent intent = new Intent(ACTION_APPWIDGET_UPDATE, null, mContext,
+ CityAppWidgetProvider.class);
+ intent.putExtra(EXTRA_APPWIDGET_IDS, new int[] {mWidgetId});
+ sendBroadcast(intent);
+
+ // Indicate successful configuration of the app widget.
+ final Intent result = new Intent().putExtra(EXTRA_APPWIDGET_ID, mWidgetId);
+ setResult(RESULT_OK, result);
+ finish();
+ }
+ }
+
+ @Override
+ public Object[] getSections() {
+ if (mSectionHeaders == null) {
+ // Make an educated guess at the expected number of sections.
+ final int approximateSectionCount = getCount() / 5;
+ final List<String> sections = new ArrayList<>(approximateSectionCount);
+ final List<Integer> positions = new ArrayList<>(approximateSectionCount);
+
+ for (int position = 0; position < getCount(); position++) {
+ // Add a section if this position should show the section index.
+ if (getShowIndex(position)) {
+ final City city = getItem(position);
+ sections.add(city.getIndexString());
+ positions.add(position);
+ }
+ }
+
+ mSectionHeaders = sections.toArray(new String[sections.size()]);
+ mSectionHeaderPositions = positions.toArray(new Integer[positions.size()]);
+ }
+ return mSectionHeaders;
+ }
+
+ @Override
+ public int getPositionForSection(int sectionIndex) {
+ return getSections().length == 0 ? 0 : mSectionHeaderPositions[sectionIndex];
+ }
+
+ @Override
+ public int getSectionForPosition(int position) {
+ if (getSections().length == 0) {
+ return 0;
+ }
+
+ for (int i = 0; i < mSectionHeaderPositions.length - 2; i++) {
+ if (position < mSectionHeaderPositions[i]) continue;
+ if (position >= mSectionHeaderPositions[i + 1]) continue;
+
+ return i;
+ }
+
+ return mSectionHeaderPositions.length - 1;
+ }
+
+ /**
+ * Clear the section headers to force them to be recomputed if they are now stale.
+ */
+ private void clearSectionHeaders() {
+ mSectionHeaders = null;
+ mSectionHeaderPositions = null;
+ }
+
+ /**
+ * Rebuilds all internal data structures from scratch.
+ */
+ private void refresh() {
+ // Update the 12/24 hour mode.
+ mIs24HoursMode = DateFormat.is24HourFormat(mContext);
+
+ // Recompute section headers.
+ clearSectionHeaders();
+
+ // Recompute filtered cities.
+ filter(mSearchMenuItemController.getQueryText());
+ }
+
+ /**
+ * Filter the cities using the given {@code queryText}.
+ */
+ private void filter(String queryText) {
+ mSearchMenuItemController.setQueryText(queryText);
+ final String query = City.removeSpecialCharacters(queryText.toUpperCase());
+ final List<City> cities = DataModel.getDataModel().getAllCities();
+
+ // Compute the filtered list of cities.
+ final List<City> filteredCities;
+ if (TextUtils.isEmpty(query)) {
+ filteredCities = cities;
+ } else {
+ filteredCities = new ArrayList<>(cities.size());
+ for (City city : cities) {
+ if (city.matches(query)) {
+ filteredCities.add(city);
+ }
+ }
+ }
+
+ // Swap in the filtered list of cities and notify of the data change.
+ mFilteredCities = filteredCities;
+ notifyDataSetChanged();
+ }
+
+ private boolean isFiltering() {
+ return !TextUtils.isEmpty(mSearchMenuItemController.getQueryText().trim());
+ }
+
+ private CharSequence getTimeCharSequence(TimeZone timeZone) {
+ mCalendar.setTimeZone(timeZone);
+ return DateFormat.format(mIs24HoursMode ? mPattern24 : mPattern12, mCalendar);
+ }
+
+ private boolean getShowIndex(int position) {
+ // Indexes are never displayed on filtered cities.
+ if (isFiltering()) {
+ return false;
+ }
+
+ // The first entry is always a header.
+ if (position == 0) {
+ return true;
+ }
+
+ // Otherwise compare the city with its predecessor to test if it is a header.
+ final City priorCity = getItem(position - 1);
+ final City city = getItem(position);
+ return mNameIndexComparator.compare(priorCity, city) != 0;
+ }
+ }
+
+ /**
+ * Cache the child views of each city item view.
+ */
+ private static final class CityItemHolder {
+
+ private final TextView index;
+ private final TextView name;
+ private final TextView time;
+
+ public CityItemHolder(TextView index, TextView name, TextView time) {
+ this.index = index;
+ this.name = name;
+ this.time = time;
+ }
+ }
+} \ No newline at end of file