1/*
2 * Copyright (C) 2016 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.TypedValue;
24import android.view.View;
25import android.widget.RemoteViews;
26import android.widget.RemoteViewsService.RemoteViewsFactory;
27
28import com.android.deskclock.LogUtils;
29import com.android.deskclock.R;
30import com.android.deskclock.Utils;
31import com.android.deskclock.data.City;
32import com.android.deskclock.data.DataModel;
33
34import java.util.ArrayList;
35import java.util.Calendar;
36import java.util.Collections;
37import java.util.List;
38import java.util.Locale;
39import java.util.TimeZone;
40
41import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID;
42import static android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID;
43import static java.util.Calendar.DAY_OF_WEEK;
44
45/**
46 * This factory produces entries in the world cities list view displayed at the bottom of the
47 * digital widget. Each row is comprised of two world cities located side-by-side.
48 */
49public class DigitalAppWidgetCityViewsFactory implements RemoteViewsFactory {
50
51    private static final LogUtils.Logger LOGGER = new LogUtils.Logger("DigWidgetViewsFactory");
52
53    private final Intent mFillInIntent = new Intent();
54
55    private final Context mContext;
56    private final float m12HourFontSize;
57    private final float m24HourFontSize;
58    private final int mWidgetId;
59    private float mFontScale = 1;
60
61    private City mHomeCity;
62    private boolean mShowHomeClock;
63    private List<City> mCities = Collections.emptyList();
64
65    public DigitalAppWidgetCityViewsFactory(Context context, Intent intent) {
66        mContext = context;
67        mWidgetId = intent.getIntExtra(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID);
68
69        final Resources res = context.getResources();
70        m12HourFontSize = res.getDimension(R.dimen.digital_widget_city_12_medium_font_size);
71        m24HourFontSize = res.getDimension(R.dimen.digital_widget_city_24_medium_font_size);
72    }
73
74    @Override
75    public void onCreate() {
76        LOGGER.i("DigitalAppWidgetCityViewsFactory onCreate " + mWidgetId);
77    }
78
79    @Override
80    public void onDestroy() {
81        LOGGER.i("DigitalAppWidgetCityViewsFactory onDestroy " + mWidgetId);
82    }
83
84    /**
85     * <p>Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and
86     * mShowHomeClock.</p>
87     *
88     * {@inheritDoc}
89     */
90    @Override
91    public synchronized int getCount() {
92        final int homeClockCount = mShowHomeClock ? 1 : 0;
93        final int worldClockCount = mCities.size();
94        final double totalClockCount = homeClockCount + worldClockCount;
95
96        // number of clocks / 2 clocks per row
97        return (int) Math.ceil(totalClockCount / 2);
98    }
99
100    /**
101     * <p>Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and
102     * mShowHomeClock.</p>
103     *
104     * {@inheritDoc}
105     */
106    @Override
107    public synchronized RemoteViews getViewAt(int position) {
108        final int homeClockOffset = mShowHomeClock ? -1 : 0;
109        final int leftIndex = position * 2 + homeClockOffset;
110        final int rightIndex = leftIndex + 1;
111
112        final City left = leftIndex == -1 ? mHomeCity :
113                (leftIndex < mCities.size() ? mCities.get(leftIndex) : null);
114        final City right = rightIndex < mCities.size() ? mCities.get(rightIndex) : null;
115
116        final RemoteViews rv =
117                new RemoteViews(mContext.getPackageName(), R.layout.world_clock_remote_list_item);
118
119        // Show the left clock if one exists.
120        if (left != null) {
121            update(rv, left, R.id.left_clock, R.id.city_name_left, R.id.city_day_left);
122        } else {
123            hide(rv, R.id.left_clock, R.id.city_name_left, R.id.city_day_left);
124        }
125
126        // Show the right clock if one exists.
127        if (right != null) {
128            update(rv, right, R.id.right_clock, R.id.city_name_right, R.id.city_day_right);
129        } else {
130            hide(rv, R.id.right_clock, R.id.city_name_right, R.id.city_day_right);
131        }
132
133        // Hide last spacer in last row; show for all others.
134        final boolean lastRow = position == getCount() - 1;
135        rv.setViewVisibility(R.id.city_spacer, lastRow ? View.GONE : View.VISIBLE);
136
137        rv.setOnClickFillInIntent(R.id.widget_item, mFillInIntent);
138        return rv;
139    }
140
141    @Override
142    public long getItemId(int position) {
143        return position;
144    }
145
146    @Override
147    public RemoteViews getLoadingView() {
148        return null;
149    }
150
151    @Override
152    public int getViewTypeCount() {
153        return 1;
154    }
155
156    @Override
157    public boolean hasStableIds() {
158        return false;
159    }
160
161    /**
162     * <p>Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and
163     * mShowHomeClock.</p>
164     *
165     * {@inheritDoc}
166     */
167    @Override
168    public synchronized void onDataSetChanged() {
169        // Fetch the data on the main Looper.
170        final RefreshRunnable refreshRunnable = new RefreshRunnable();
171        DataModel.getDataModel().run(refreshRunnable);
172
173        // Store the data in local variables.
174        mHomeCity = refreshRunnable.mHomeCity;
175        mCities = refreshRunnable.mCities;
176        mShowHomeClock = refreshRunnable.mShowHomeClock;
177        mFontScale = WidgetUtils.getScaleRatio(mContext, null, mWidgetId, mCities.size());
178    }
179
180    private void update(RemoteViews rv, City city, int clockId, int labelId, int dayId) {
181        rv.setCharSequence(clockId, "setFormat12Hour", Utils.get12ModeFormat(0.4f, false));
182        rv.setCharSequence(clockId, "setFormat24Hour", Utils.get24ModeFormat(false));
183
184        final boolean is24HourFormat = DateFormat.is24HourFormat(mContext);
185        final float fontSize = is24HourFormat ? m24HourFontSize : m12HourFontSize;
186        rv.setTextViewTextSize(clockId, TypedValue.COMPLEX_UNIT_PX, fontSize * mFontScale);
187        rv.setString(clockId, "setTimeZone", city.getTimeZone().getID());
188        rv.setTextViewText(labelId, city.getName());
189
190        // Compute if the city week day matches the weekday of the current timezone.
191        final Calendar localCal = Calendar.getInstance(TimeZone.getDefault());
192        final Calendar cityCal = Calendar.getInstance(city.getTimeZone());
193        final boolean displayDayOfWeek = localCal.get(DAY_OF_WEEK) != cityCal.get(DAY_OF_WEEK);
194
195        // Bind the week day display.
196        if (displayDayOfWeek) {
197            final Locale locale = Locale.getDefault();
198            final String weekday = cityCal.getDisplayName(DAY_OF_WEEK, Calendar.SHORT, locale);
199            final String slashDay = mContext.getString(R.string.world_day_of_week_label, weekday);
200            rv.setTextViewText(dayId, slashDay);
201        }
202
203        rv.setViewVisibility(dayId, displayDayOfWeek ? View.VISIBLE : View.GONE);
204        rv.setViewVisibility(clockId, View.VISIBLE);
205        rv.setViewVisibility(labelId, View.VISIBLE);
206    }
207
208    private void hide(RemoteViews clock, int clockId, int labelId, int dayId) {
209        clock.setViewVisibility(dayId, View.INVISIBLE);
210        clock.setViewVisibility(clockId, View.INVISIBLE);
211        clock.setViewVisibility(labelId, View.INVISIBLE);
212    }
213
214    /**
215     * This Runnable fetches data for this factory on the main thread to ensure all DataModel reads
216     * occur on the main thread.
217     */
218    private static final class RefreshRunnable implements Runnable {
219
220        private City mHomeCity;
221        private List<City> mCities;
222        private boolean mShowHomeClock;
223
224        @Override
225        public void run() {
226            mHomeCity = DataModel.getDataModel().getHomeCity();
227            mCities = new ArrayList<>(DataModel.getDataModel().getSelectedCities());
228            mShowHomeClock = DataModel.getDataModel().getShowHomeClock();
229        }
230    }
231}
232