1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.alarmclock;
18
19import android.content.Context;
20import android.content.Intent;
21import android.content.res.Resources;
22import android.text.format.DateFormat;
23import android.util.Log;
24import android.util.TypedValue;
25import android.view.View;
26import android.widget.RemoteViews;
27import android.widget.RemoteViewsService.RemoteViewsFactory;
28
29import com.android.deskclock.R;
30import com.android.deskclock.data.City;
31import com.android.deskclock.data.DataModel;
32
33import java.util.ArrayList;
34import java.util.Calendar;
35import java.util.List;
36import java.util.Locale;
37import java.util.TimeZone;
38
39import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID;
40import static android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID;
41import static com.android.deskclock.Utils.enforceMainLooper;
42import static java.util.Calendar.DAY_OF_WEEK;
43
44public class DigitalWidgetViewsFactory implements RemoteViewsFactory {
45
46    private static final String TAG = "DigWidgetViewsFactory";
47
48    private final Intent mFillInIntent = new Intent();
49
50    private final Context mContext;
51    private final Resources mResources;
52    private final float mFontSize;
53    private final float mFont24Size;
54    private final int mWidgetId;
55    private float mFontScale = 1;
56
57    private City mHomeCity;
58    private List<City> mCities;
59    private boolean mShowHomeClock;
60
61    public DigitalWidgetViewsFactory(Context context, Intent intent) {
62        mContext = context;
63        mResources = context.getResources();
64        mFontSize = mResources.getDimension(R.dimen.widget_medium_font_size);
65        mFont24Size = mResources.getDimension(R.dimen.widget_24_medium_font_size);
66        mWidgetId = intent.getIntExtra(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID);
67    }
68
69    @Override
70    public void onCreate() {
71        if (DigitalAppWidgetService.LOGGING) {
72            Log.i(TAG, "DigitalWidget onCreate " + mWidgetId);
73        }
74    }
75
76    @Override
77    public void onDestroy() {
78        if (DigitalAppWidgetService.LOGGING) {
79            Log.i(TAG, "DigitalWidget onDestroy " + mWidgetId);
80        }
81    }
82
83    /**
84     * <p>Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and
85     * mShowHomeClock.</p>
86     *
87     * {@inheritDoc}
88     */
89    @Override
90    public synchronized int getCount() {
91        if (!WidgetUtils.showList(mContext, mWidgetId, mFontScale)) {
92            return 0;
93        }
94
95        final int homeClockCount = mShowHomeClock ? 1 : 0;
96        final int worldClockCount = mCities.size();
97        final double totalClockCount = homeClockCount + worldClockCount;
98
99        // number of clocks / 2 clocks per row
100        return (int) Math.ceil(totalClockCount / 2);
101    }
102
103    /**
104     * <p>Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and
105     * mShowHomeClock.</p>
106     *
107     * {@inheritDoc}
108     */
109    @Override
110    public synchronized RemoteViews getViewAt(int position) {
111        final int homeClockOffset = mShowHomeClock ? -1 : 0;
112        final int leftIndex = position * 2 + homeClockOffset;
113        final int rightIndex = leftIndex + 1;
114
115        final City left = leftIndex == -1 ? mHomeCity :
116                (leftIndex < mCities.size() ? mCities.get(leftIndex) : null);
117        final City right = rightIndex < mCities.size() ? mCities.get(rightIndex) : null;
118
119        final RemoteViews clock =
120                new RemoteViews(mContext.getPackageName(), R.layout.world_clock_remote_list_item);
121
122        // Show the left clock if one exists.
123        if (left != null) {
124            update(clock, left, R.id.left_clock, R.id.city_name_left, R.id.city_day_left);
125        } else {
126            hide(clock, R.id.left_clock, R.id.city_name_left, R.id.city_day_left);
127        }
128
129        // Show the right clock if one exists.
130        if (right != null) {
131            update(clock, right, R.id.right_clock, R.id.city_name_right, R.id.city_day_right);
132        } else {
133            hide(clock, R.id.right_clock, R.id.city_name_right, R.id.city_day_right);
134        }
135
136        // Hide last spacer in last row; show for all others.
137        final boolean lastRow = position == getCount() - 1;
138        clock.setViewVisibility(R.id.city_spacer, lastRow ? View.GONE : View.VISIBLE);
139
140        clock.setOnClickFillInIntent(R.id.widget_item, mFillInIntent);
141        return clock;
142    }
143
144    @Override
145    public long getItemId(int position) {
146        return position;
147    }
148
149    @Override
150    public RemoteViews getLoadingView() {
151        return null;
152    }
153
154    @Override
155    public int getViewTypeCount() {
156        return 1;
157    }
158
159    @Override
160    public boolean hasStableIds() {
161        return true;
162    }
163
164    /**
165     * <p>Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and
166     * mShowHomeClock.</p>
167     *
168     * {@inheritDoc}
169     */
170    @Override
171    public synchronized void onDataSetChanged() {
172        // Fetch the data on the main Looper.
173        final RefreshRunnable refreshRunnable = new RefreshRunnable();
174        DataModel.getDataModel().run(refreshRunnable);
175
176        // Store the data in local variables.
177        mFontScale = WidgetUtils.getScaleRatio(mContext, null, mWidgetId);
178        mHomeCity = refreshRunnable.mHomeCity;
179        mCities = refreshRunnable.mCities;
180        mShowHomeClock = refreshRunnable.mShowHomeClock;
181    }
182
183    private void update(RemoteViews clock, City city, int clockId, int labelId, int dayId) {
184        WidgetUtils.setTimeFormat(mContext, clock, true /* showAmPm */, clockId);
185
186        final float fontSize = DateFormat.is24HourFormat(mContext) ? mFont24Size : mFontSize;
187        clock.setTextViewTextSize(clockId, TypedValue.COMPLEX_UNIT_PX, fontSize * mFontScale);
188        clock.setString(clockId, "setTimeZone", city.getTimeZoneId());
189        clock.setTextViewText(labelId, city.getName());
190
191        // Compute if the city week day matches the weekday of the current timezone.
192        final Calendar localCal = Calendar.getInstance(TimeZone.getDefault());
193        final Calendar cityCal = Calendar.getInstance(city.getTimeZone());
194        final boolean displayDayOfWeek = localCal.get(DAY_OF_WEEK) != cityCal.get(DAY_OF_WEEK);
195
196        // Bind the week day display.
197        if (displayDayOfWeek) {
198            final Locale locale = Locale.getDefault();
199            final String weekday = cityCal.getDisplayName(DAY_OF_WEEK, Calendar.SHORT, locale);
200            final String slashDay = mContext.getString(R.string.world_day_of_week_label, weekday);
201            clock.setTextViewText(dayId, slashDay);
202        }
203
204        clock.setViewVisibility(dayId, displayDayOfWeek ? View.VISIBLE : View.GONE);
205        clock.setViewVisibility(clockId, View.VISIBLE);
206        clock.setViewVisibility(labelId, View.VISIBLE);
207    }
208
209    private void hide(RemoteViews clock, int clockId, int labelId, int dayId) {
210        clock.setViewVisibility(dayId, View.INVISIBLE);
211        clock.setViewVisibility(clockId, View.INVISIBLE);
212        clock.setViewVisibility(labelId, View.INVISIBLE);
213    }
214
215    /**
216     * This Runnable fetches data for this factory on the main thread to ensure all DataModel reads
217     * occur on the main thread.
218     */
219    private static final class RefreshRunnable implements Runnable {
220
221        private City mHomeCity;
222        private List<City> mCities;
223        private boolean mShowHomeClock;
224
225        @Override
226        public void run() {
227            enforceMainLooper();
228
229            mHomeCity = DataModel.getDataModel().getHomeCity();
230            mCities = new ArrayList<>(DataModel.getDataModel().getSelectedCities());
231            mShowHomeClock = DataModel.getDataModel().getShowHomeClock();
232        }
233    }
234}