blob: 075e1c3903ac73a8a1b0f4e14c1b4da9f8c34511 [file] [log] [blame]
/*
* Copyright (C) 2012 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;
import android.app.Activity;
import android.app.AlarmManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.Settings;
import android.support.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextClock;
import android.widget.TextView;
import com.android.deskclock.data.City;
import com.android.deskclock.data.DataModel;
import com.android.deskclock.uidata.UiDataModel;
import com.android.deskclock.worldclock.CitySelectionActivity;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import static android.app.AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static com.android.deskclock.uidata.UiDataModel.Tab.CLOCKS;
import static java.util.Calendar.DAY_OF_WEEK;
/**
* Fragment that shows the clock (analog or digital), the next alarm info and the world clock.
*/
public final class ClockFragment extends DeskClockFragment {
// Updates dates in the UI on every quarter-hour.
private final Runnable mQuarterHourUpdater = new QuarterHourRunnable();
// Updates the UI in response to changes to the scheduled alarm.
private BroadcastReceiver mAlarmChangeReceiver;
// Detects changes to the next scheduled alarm pre-L.
private ContentObserver mAlarmObserver;
private Handler mHandler;
private TextClock mDigitalClock;
private View mAnalogClock, mClockFrame;
private View mHairline;
private SelectedCitiesAdapter mCityAdapter;
private ListView mCityList;
private String mDateFormat;
private String mDateFormatForAccessibility;
/** The public no-arg constructor required by all fragments. */
public ClockFragment() {
super(CLOCKS);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler = new Handler();
mAlarmObserver = Utils.isPreL() ? new AlarmObserverPreL(mHandler) : null;
mAlarmChangeReceiver = Utils.isLOrLater() ? new AlarmChangedBroadcastReceiver() : null;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle icicle) {
super.onCreateView(inflater, container, icicle);
final OnTouchListener startScreenSaverListener = new StartScreenSaverListener();
final View fragmentView = inflater.inflate(R.layout.clock_fragment, container, false);
mCityAdapter = new SelectedCitiesAdapter(getActivity());
mCityList = (ListView) fragmentView.findViewById(R.id.cities);
mCityList.setDivider(null);
mCityList.setAdapter(mCityAdapter);
mCityList.setOnTouchListener(startScreenSaverListener);
mCityList.setOnScrollListener(new VerticalScrollPositionUpdater());
fragmentView.setOnTouchListener(startScreenSaverListener);
// On tablet landscape, the clock frame will be a distinct view. Otherwise, it'll be added
// on as a header to the main listview.
mClockFrame = fragmentView.findViewById(R.id.main_clock_left_pane);
if (mClockFrame == null) {
mClockFrame = inflater.inflate(R.layout.main_clock_frame, mCityList, false);
mCityList.addHeaderView(mClockFrame, null, false);
mHairline = mClockFrame.findViewById(R.id.hairline);
} else {
final View hairline = mClockFrame.findViewById(R.id.hairline);
hairline.setVisibility(GONE);
}
mDigitalClock = (TextClock) mClockFrame.findViewById(R.id.digital_clock);
mAnalogClock = mClockFrame.findViewById(R.id.analog_clock);
return fragmentView;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Utils.setTimeFormat(mDigitalClock);
}
@Override
public void onResume() {
super.onResume();
final Activity activity = getActivity();
mDateFormat = getString(R.string.abbrev_wday_month_day_no_year);
mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year);
// Watch for system events that effect clock time or format.
if (mAlarmChangeReceiver != null) {
final IntentFilter filter = new IntentFilter(ACTION_NEXT_ALARM_CLOCK_CHANGED);
activity.registerReceiver(mAlarmChangeReceiver, filter);
}
// Resume can be invoked after changing the clock style.
Utils.setClockStyle(mDigitalClock, mAnalogClock);
final View view = getView();
if (view != null && view.findViewById(R.id.main_clock_left_pane) != null) {
// Center the main clock frame by hiding the world clocks when none are selected.
mCityList.setVisibility(mCityAdapter.getCount() == 0 ? GONE : VISIBLE);
}
// In portrait, the hairline is shown only when the adapter contains cities.
if (mHairline != null) {
mHairline.setVisibility(mCityAdapter.getCount() == 0 ? GONE : VISIBLE);
}
refreshDates();
refreshAlarm();
// Alarm observer is null on L or later.
if (mAlarmObserver != null) {
@SuppressWarnings("deprecation")
final Uri uri = Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED);
activity.getContentResolver().registerContentObserver(uri, false, mAlarmObserver);
}
// Schedule a runnable to update the date every quarter hour.
UiDataModel.getUiDataModel().addQuarterHourCallback(mQuarterHourUpdater, 100);
}
@Override
public void onPause() {
super.onPause();
UiDataModel.getUiDataModel().removePeriodicCallback(mQuarterHourUpdater);
final Activity activity = getActivity();
if (mAlarmChangeReceiver != null) {
activity.unregisterReceiver(mAlarmChangeReceiver);
}
if (mAlarmObserver != null) {
activity.getContentResolver().unregisterContentObserver(mAlarmObserver);
}
}
@Override
public void onFabClick(@NonNull ImageView fab) {
startActivity(new Intent(getActivity(), CitySelectionActivity.class));
}
@Override
public void onUpdateFab(@NonNull ImageView fab) {
fab.setVisibility(VISIBLE);
fab.setImageResource(R.drawable.ic_language);
fab.setContentDescription(fab.getResources().getString(R.string.button_cities));
}
@Override
public void onUpdateFabButtons(@NonNull ImageButton left, @NonNull ImageButton right) {
left.setVisibility(INVISIBLE);
right.setVisibility(INVISIBLE);
}
/**
* Refresh the displayed dates in response to a change that may have changed them.
*/
private void refreshDates() {
// Refresh the date in the main clock.
Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mClockFrame);
// Refresh the day-of-week in each world clock.
mCityAdapter.notifyDataSetChanged();
}
/**
* Refresh the next alarm time.
*/
private void refreshAlarm() {
Utils.refreshAlarm(getActivity(), mClockFrame);
}
/**
* Long pressing over the main clock or any world clock item starts the screen saver.
*/
private final class StartScreenSaverListener implements OnTouchListener, Runnable {
private float mTouchSlop = -1;
private int mLongPressTimeout = -1;
private float mLastTouchX, mLastTouchY;
@Override
public boolean onTouch(View v, MotionEvent event) {
if (mTouchSlop == -1) {
mTouchSlop = ViewConfiguration.get(getActivity()).getScaledTouchSlop();
mLongPressTimeout = ViewConfiguration.getLongPressTimeout();
}
switch (event.getAction()) {
case (MotionEvent.ACTION_DOWN):
// Create and post a runnable to start the screen saver in the future.
mHandler.postDelayed(this, mLongPressTimeout);
mLastTouchX = event.getX();
mLastTouchY = event.getY();
return true;
case (MotionEvent.ACTION_MOVE):
final float xDiff = Math.abs(event.getX() - mLastTouchX);
final float yDiff = Math.abs(event.getY() - mLastTouchY);
if (xDiff >= mTouchSlop || yDiff >= mTouchSlop) {
mHandler.removeCallbacks(this);
}
break;
default:
mHandler.removeCallbacks(this);
}
return false;
}
@Override
public void run() {
startActivity(new Intent(getActivity(), ScreensaverActivity.class)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}
}
/**
* Updates the vertical scroll state of this tab in the {@link UiDataModel} as it changes.
*/
private final class VerticalScrollPositionUpdater implements AbsListView.OnScrollListener {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
setTabScrolledToTop(Utils.isScrolledToTop(view));
}
}
/**
* This runnable executes at every quarter-hour (e.g. 1:00, 1:15, 1:30, 1:45, etc...) and
* updates the dates displayed within the UI. Quarter-hour increments were chosen to accommodate
* the "weirdest" timezones (e.g. Nepal is UTC/GMT +05:45).
*/
private final class QuarterHourRunnable implements Runnable {
@Override
public void run() {
refreshDates();
}
}
/**
* Prior to L, a ContentObserver was used to monitor changes to the next scheduled alarm.
* In L and beyond this is accomplished via a system broadcast of
* {@link AlarmManager#ACTION_NEXT_ALARM_CLOCK_CHANGED}.
*/
private final class AlarmObserverPreL extends ContentObserver {
public AlarmObserverPreL(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
Utils.refreshAlarm(getActivity(), mClockFrame);
}
}
/**
* Update the display of the scheduled alarm as it changes.
*/
private final class AlarmChangedBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
refreshAlarm();
}
}
/**
* This adapter lists all of the selected world clocks. Optionally, it also includes a clock at
* the top for the home timezone if "Automatic home clock" is turned on in settings and the
* current time at home does not match the current time in the timezone of the current location.
*/
private static final class SelectedCitiesAdapter extends BaseAdapter {
private final LayoutInflater mInflater;
private final Context mContext;
private final boolean mIsLandscape;
public SelectedCitiesAdapter(Context context) {
mContext = context;
mInflater = LayoutInflater.from(context);
mIsLandscape = Utils.isLandscape(context);
}
@Override
public int getCount() {
final int homeClockCount = getShowHomeClock() ? 1 : 0;
final int worldClockCount = getCities().size();
return homeClockCount + worldClockCount;
}
@Override
public Object getItem(int position) {
if (getShowHomeClock()) {
return position == 0 ? getHomeCity() : getCities().get(position - 1);
}
return getCities().get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View view, ViewGroup parent) {
// Retrieve the city to bind.
final City city = (City) getItem(position);
// Inflate a new view for the city, if necessary.
if (view == null) {
view = mInflater.inflate(R.layout.world_clock_item, parent, false);
}
// Configure the digital clock or analog clock depending on the user preference.
final TextClock digitalClock = (TextClock) view.findViewById(R.id.digital_clock);
final AnalogClock analogClock = (AnalogClock) view.findViewById(R.id.analog_clock);
if (DataModel.getDataModel().getClockStyle() == DataModel.ClockStyle.ANALOG) {
digitalClock.setVisibility(GONE);
analogClock.setVisibility(VISIBLE);
analogClock.setTimeZone(city.getTimeZone().getID());
analogClock.enableSeconds(false);
} else {
analogClock.setVisibility(GONE);
digitalClock.setVisibility(VISIBLE);
digitalClock.setTimeZone(city.getTimeZone().getID());
digitalClock.setFormat12Hour(Utils.get12ModeFormat(0.22f /* amPmRatio */));
digitalClock.setFormat24Hour(Utils.get24ModeFormat());
}
// Supply top and bottom padding dynamically.
final Resources res = mContext.getResources();
final int padding = res.getDimensionPixelSize(R.dimen.medium_space_top);
final int top = position == 0 && mIsLandscape ? 0 : padding;
final int left = view.getPaddingLeft();
final int right = view.getPaddingRight();
final int bottom = view.getPaddingBottom();
view.setPadding(left, top, right, bottom);
// Bind the city name.
final TextView name = (TextView) view.findViewById(R.id.city_name);
name.setText(city.getName());
// Compute if the city week day matches the weekday of the current timezone.
final Calendar localCal = Calendar.getInstance(TimeZone.getDefault());
final Calendar cityCal = Calendar.getInstance(city.getTimeZone());
final boolean displayDayOfWeek = localCal.get(DAY_OF_WEEK) != cityCal.get(DAY_OF_WEEK);
// Bind the week day display.
final TextView dayOfWeek = (TextView) view.findViewById(R.id.city_day);
dayOfWeek.setVisibility(displayDayOfWeek ? VISIBLE : GONE);
if (displayDayOfWeek) {
final Locale locale = Locale.getDefault();
final String weekday = cityCal.getDisplayName(DAY_OF_WEEK, Calendar.SHORT, locale);
dayOfWeek.setText(mContext.getString(R.string.world_day_of_week_label, weekday));
}
return view;
}
/**
* @return {@code false} to prevent the cities from responding to touch
*/
@Override
public boolean isEnabled(int position) {
return false;
}
private City getHomeCity() {
return DataModel.getDataModel().getHomeCity();
}
private List<City> getCities() {
return DataModel.getDataModel().getSelectedCities();
}
private boolean getShowHomeClock() {
return DataModel.getDataModel().getShowHomeClock();
}
}
}