/*
* Copyright 2014 Google Inc. All rights reserved.
*
* 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.google.samples.apps.iosched.myschedule;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.design.widget.TabLayout;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPager;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
import android.widget.ScrollView;
import android.widget.TextView;
import com.google.samples.apps.iosched.Config;
import com.google.samples.apps.iosched.R;
import com.google.samples.apps.iosched.archframework.PresenterImpl;
import com.google.samples.apps.iosched.archframework.UpdatableView;
import com.google.samples.apps.iosched.injection.ModelProvider;
import com.google.samples.apps.iosched.model.ScheduleHelper;
import com.google.samples.apps.iosched.navigation.NavigationModel;
import com.google.samples.apps.iosched.session.SessionDetailActivity;
import com.google.samples.apps.iosched.ui.BaseActivity;
import com.google.samples.apps.iosched.util.AnalyticsHelper;
import com.google.samples.apps.iosched.util.TimeUtils;
import com.google.samples.apps.iosched.util.UIUtils;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import static com.google.samples.apps.iosched.util.LogUtils.LOGD;
import static com.google.samples.apps.iosched.util.LogUtils.LOGE;
import static com.google.samples.apps.iosched.util.LogUtils.makeLogTag;
/**
* This shows the schedule of the logged in user, organised per day.
* <p/>
* Depending on the device, this Activity uses either a {@link ViewPager} with a {@link
* MyScheduleSingleDayFragment} for its page (the "narrow" layout) or a {@link
* MyScheduleAllDaysFragment}, which uses a {@link MyScheduleSingleDayNoScrollView} for each day.
* Each day data is backed by a {@link MyScheduleDayAdapter} (the "wide" layout).
* <p/>
* If the user attends the conference, all time slots that have sessions are shown, with a button to
* allow the user to see all sessions in that slot.
*/
public class MyScheduleActivity extends BaseActivity implements
MyScheduleSingleDayFragment.Listener {
/**
* This is used in the narrow mode, to pass in the day index to the {@link
* MyScheduleSingleDayFragment}.
*/
public static final String ARG_CONFERENCE_DAY_INDEX
= "com.google.samples.apps.iosched.ARG_CONFERENCE_DAY_INDEX";
public static final String EXTRA_DIALOG_TITLE
= "com.google.samples.apps.iosched.EXTRA_DIALOG_TITLE";
public static final String EXTRA_DIALOG_MESSAGE
= "com.google.samples.apps.iosched.EXTRA_DIALOG_MESSAGE";
public static final String EXTRA_DIALOG_YES
= "com.google.samples.apps.iosched.EXTRA_DIALOG_YES";
public static final String EXTRA_DIALOG_NO
= "com.google.samples.apps.iosched.EXTRA_DIALOG_NO";
public static final String EXTRA_DIALOG_URL
= "com.google.samples.apps.iosched.EXTRA_DIALOG_URL";
/**
* Interval that a timer will redraw the UI during the conference, so that time sensitive
* widgets, like the "Now" and "Ended" indicators can be properly updated.
*/
private static final long INTERVAL_TO_REDRAW_UI = 1 * TimeUtils.MINUTE;
/**
* The key used to save the tags for {@link MyScheduleSingleDayFragment}s so the automatically
* recreated fragments can be reused by {@link #mViewPagerAdapter}.
*/
private static final String SINGLE_DAY_FRAGMENTS_TAGS = "single_day_fragments_tags";
/**
* The key used to save the position in the {@link #mViewPagerAdapter} for the current {@link
* MyScheduleSingleDayFragment}s.
*/
private static final String CURRENT_SINGLE_DAY_FRAGMENT_POSITION =
"current_single_day_fragments_position";
private static final String SCREEN_LABEL = "My Schedule";
private static final String TAG = makeLogTag(MyScheduleActivity.class);
public static int BASE_TAB_VIEW_ID = 12345;
/**
* If true, we are in the wide (tablet landscape) mode where we show conference days side by
* side; if false, we are in narrow (non tablet landscape) mode where we use a ViewPager and
* show one conference day per page.
*/
private boolean mWideMode = false;
/**
* This is used for narrow mode only, to switch between days, it is null in wide mode
*/
private ViewPager mViewPager;
/**
* This is used for narrow mode only, it is empty in wide mode
*/
private Set<MyScheduleSingleDayFragment> mMyScheduleSingleDayFragments
= new HashSet<MyScheduleSingleDayFragment>();
/**
* This is used for narrow mode only, it is null in wide mode. Each page in the {@link
* #mViewPager} is a {@link MyScheduleSingleDayFragment}.
*/
private MyScheduleDayViewPagerAdapter mViewPagerAdapter;
/**
* This is used for narrow mode only, to display the conference days, it is null in wide mode
*/
private TabLayout mTabLayout;
/**
* This is used in wide mode only, it is null in narrow mode
*/
private ScrollView mScrollViewWide;
/**
* This is a view displayed when login has failed
*/
private View mFailedLoginView;
/**
* During the conference, this is set to the current day, eg 1 for the first day, 2 for the
* second etc Outside of conference period, this is set to 1.
*/
private int mToday;
/**
* True during the conference or pre conference, false otherwise
*/
private boolean mConferenceInProgress;
private boolean mDestroyed = false;
private boolean mShowedAnnouncementDialog = false;
private PresenterImpl mPresenter;
@Override
protected NavigationModel.NavigationItemEnum getSelfNavDrawerItem() {
return NavigationModel.NavigationItemEnum.MY_SCHEDULE;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.my_schedule_act);
launchSessionDetailIfRequiredByIntent(getIntent());
// ANALYTICS SCREEN: View the My Schedule screen
// Contains: Nothing (Page name is a constant)
AnalyticsHelper.sendScreenView(SCREEN_LABEL);
String[] singleDayFragmentsTags = null;
int currentSingleDayFragment = 0;
if (savedInstanceState != null &&
savedInstanceState.containsKey(SINGLE_DAY_FRAGMENTS_TAGS)) {
singleDayFragmentsTags = savedInstanceState.getStringArray(SINGLE_DAY_FRAGMENTS_TAGS);
}
if (savedInstanceState != null &&
savedInstanceState.containsKey(CURRENT_SINGLE_DAY_FRAGMENT_POSITION)) {
currentSingleDayFragment =
savedInstanceState.getInt(CURRENT_SINGLE_DAY_FRAGMENT_POSITION);
}
initViews(singleDayFragmentsTags, currentSingleDayFragment);
initPresenter();
overridePendingTransition(0, 0);
}
@Override
public void onResume() {
super.onResume();
calculateCurrentDay();
if (mConferenceInProgress) {
scheduleNextUIUpdate();
}
showAnnouncementDialogIfNeeded(getIntent());
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mViewPagerAdapter != null && mViewPagerAdapter.getFragments() != null) {
MyScheduleSingleDayFragment[] singleDayFragments = mViewPagerAdapter.getFragments();
String[] tags = new String[singleDayFragments.length];
for (int i = 0; i < tags.length; i++) {
tags[i] = singleDayFragments[i].getTag();
}
outState.putStringArray(SINGLE_DAY_FRAGMENTS_TAGS, tags);
outState.putInt(CURRENT_SINGLE_DAY_FRAGMENT_POSITION, mViewPager.getCurrentItem());
}
}
@Override
protected void onDestroy() {
super.onDestroy();
mDestroyed = true;
}
/**
* Pre-process the {@code intent} received to open this activity to determine if it was a deep
* link to a SessionDetail. Typically you wouldn't use this type of logic, but we need to
* because of the path of session details page on the website is only /schedule and session ids
* are part of the query parameters ("sid").
*/
private void launchSessionDetailIfRequiredByIntent(Intent intent) {
if (intent != null && !TextUtils.isEmpty(intent.getDataString())) {
String intentDataString = intent.getDataString();
try {
Uri dataUri = Uri.parse(intentDataString);
// Website sends sessionId in query parameter "sid". If present, show
// SessionDetailActivity
String sessionId = dataUri.getQueryParameter("sid");
if (!TextUtils.isEmpty(sessionId)) {
LOGD(TAG, "SessionId received from website: " + sessionId);
SessionDetailActivity.startSessionDetailActivity(MyScheduleActivity.this,
sessionId);
finish();
} else {
LOGD(TAG, "No SessionId received from website");
}
} catch (Exception exception) {
LOGE(TAG, "Data uri existing but wasn't parsable for a session detail deep link");
}
}
}
/**
* @param singleDayFragmentsTags The tags of the recreated fragments, if this is an Activity
* recreation, or null
* @param currentSingleDayFragment The position of the current single day fragment (ie the
* position of the current tab)
*/
private void initViews(String[] singleDayFragmentsTags, int currentSingleDayFragment) {
// Set up view to show login failure
mFailedLoginView = findViewById(R.id.butter_bar);
hideLoginFailureView();
// Set up correct view mode
detectNarrowOrWideMode();
if (mWideMode) {
setUpViewForWideMode();
} else {
setUpViewPagerForNarrowMode(singleDayFragmentsTags, currentSingleDayFragment);
}
}
private void initPresenter() {
MyScheduleModel model =
ModelProvider.provideMyScheduleModel(new ScheduleHelper(this), this);
if (mWideMode) {
mPresenter = new PresenterImpl(model,
(UpdatableView) getFragmentManager().findFragmentById(R.id.myScheduleWideFrag),
MyScheduleModel.MyScheduleUserActionEnum.values(),
MyScheduleModel.MyScheduleQueryEnum.values());
mPresenter.loadInitialQueries();
} else {
// Each fragment in the pager adapter is an updatable view that the presenter must know
MyScheduleSingleDayFragment[] fragments = mViewPagerAdapter.getFragments();
UpdatableView[] views = new UpdatableView[fragments.length];
for (int i = 0; i < fragments.length; i++) {
views[i] = fragments[i];
}
mPresenter = new PresenterImpl(model, views,
MyScheduleModel.MyScheduleUserActionEnum.values(),
MyScheduleModel.MyScheduleQueryEnum.values());
}
}
private void detectNarrowOrWideMode() {
// When changing orientation, if previously in wide mode, the system recreates the wide
// fragment, so need to check also that view pager isn't visible
mWideMode = getFragmentManager().findFragmentById(R.id.myScheduleWideFrag) != null &&
findViewById(R.id.view_pager).getVisibility() == View.GONE;
}
private void setUpViewForWideMode() {
mScrollViewWide = (ScrollView) findViewById(R.id.main_content_wide);
// Nothing else to do, as wide mode only uses MyScheduleAllDaysFragment, which will set
// itself up
}
/**
* @param singleDayFragmentsTags The tags of the recreated fragments, if this is an Activity
* recreation, or null
* @param currentSingleDayFragment The position of the current single day fragment (ie the
* position of the current tab)
*/
private void setUpViewPagerForNarrowMode(String[] singleDayFragmentsTags,
int currentSingleDayFragment) {
mViewPager = (ViewPager) findViewById(R.id.view_pager);
mViewPagerAdapter = new MyScheduleDayViewPagerAdapter(this, getFragmentManager(),
MyScheduleModel.showPreConferenceData(this));
mViewPagerAdapter.setRetainedFragmentsTags(singleDayFragmentsTags);
mViewPager.setAdapter(mViewPagerAdapter);
mViewPager.setCurrentItem(currentSingleDayFragment);
mTabLayout = (TabLayout) findViewById(R.id.sliding_tabs);
mTabLayout.setupWithViewPager(mViewPager);
mTabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
mViewPager.setCurrentItem(tab.getPosition(), true);
TextView view = (TextView) findViewById(BASE_TAB_VIEW_ID + tab.getPosition());
view.setContentDescription(
getString(R.string.talkback_selected,
getString(R.string.a11y_button, tab.getText())));
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
TextView view = (TextView) findViewById(BASE_TAB_VIEW_ID + tab.getPosition());
view.setContentDescription(
getString(R.string.a11y_button, tab.getText()));
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
// Do nothing
}
});
mViewPager.setPageMargin(getResources()
.getDimensionPixelSize(R.dimen.my_schedule_page_margin));
mViewPager.setPageMarginDrawable(R.drawable.page_margin);
setTabLayoutContentDescriptionsForNarrowLayout();
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
calculateCurrentDay();
if (mViewPager != null) {
showDay(mToday);
}
}
private void calculateCurrentDay() {
long now = TimeUtils.getCurrentTime(this);
// If we are before or after the conference, the first day is considered the current day
mToday = 1;
mConferenceInProgress = false;
for (int i = 0; i < Config.CONFERENCE_DAYS.length; i++) {
if (now >= Config.CONFERENCE_DAYS[i][0] && now <= Config.CONFERENCE_DAYS[i][1]) {
// mToday is set to 1 for the first day, 2 for the second etc
mToday = i + 1;
mConferenceInProgress = true;
break;
}
}
}
/**
* @param day Pass in 1 for the first day, 2 for the second etc
*/
private void showDay(int day) {
int preConferenceDays = MyScheduleModel.showPreConferenceData(this) ? 1 : 0;
mViewPager.setCurrentItem(day - 1 + preConferenceDays);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
launchSessionDetailIfRequiredByIntent(intent);
LOGD(TAG, "onNewIntent, extras " + intent.getExtras());
if (intent.hasExtra(EXTRA_DIALOG_MESSAGE)) {
mShowedAnnouncementDialog = false;
showAnnouncementDialogIfNeeded(intent);
}
}
private void setTabLayoutContentDescriptionsForNarrowLayout() {
LayoutInflater inflater = getLayoutInflater();
int gap = MyScheduleModel.showPreConferenceData(this) ? 1 : 0;
for (int i = 0, count = mTabLayout.getTabCount(); i < count; i++) {
TabLayout.Tab tab = mTabLayout.getTabAt(i);
TextView view =
(TextView) inflater.inflate(R.layout.tab_my_schedule, mTabLayout, false);
view.setId(BASE_TAB_VIEW_ID + i);
view.setText(tab.getText());
if (i == 0) {
view.setContentDescription(
getString(R.string.talkback_selected,
getString(R.string.a11y_button, tab.getText())));
} else {
view.setContentDescription(
getString(R.string.a11y_button, tab.getText()));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
view.announceForAccessibility(
getString(R.string.my_schedule_tab_desc_a11y,
TimeUtils.getDayName(this, i - gap)));
}
tab.setCustomView(view);
}
}
private void hideLoginFailureView() {
mFailedLoginView.setVisibility(View.GONE);
}
@Override
public void onAuthFailure(String accountName) {
super.onAuthFailure(accountName);
UIUtils.setUpButterBar(mFailedLoginView, getString(R.string.login_failed_text),
getString(R.string.login_failed_text_retry), new View.OnClickListener() {
@Override
public void onClick(View v) {
hideLoginFailureView();
retryAuth();
}
}
);
}
@Override
public void onAccountChangeRequested() {
super.onAccountChangeRequested();
hideLoginFailureView();
reloadData();
}
private void reloadData() {
if (mPresenter != null) {
mPresenter.onUserAction(MyScheduleModel.MyScheduleUserActionEnum.RELOAD_DATA, null);
}
}
@Override
public boolean canSwipeRefreshChildScrollUp() {
if (mWideMode) {
return ViewCompat.canScrollVertically(mScrollViewWide, -1);
}
for (MyScheduleSingleDayFragment fragment : mMyScheduleSingleDayFragments) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
if (!fragment.getUserVisibleHint()) {
continue;
}
}
return ViewCompat.canScrollVertically(fragment.getListView(), -1);
}
return false;
}
private void showAnnouncementDialogIfNeeded(Intent intent) {
final String title = intent.getStringExtra(EXTRA_DIALOG_TITLE);
final String message = intent.getStringExtra(EXTRA_DIALOG_MESSAGE);
if (!mShowedAnnouncementDialog && !TextUtils.isEmpty(title) && !TextUtils
.isEmpty(message)) {
LOGD(TAG, "showAnnouncementDialogIfNeeded, title: " + title);
LOGD(TAG, "showAnnouncementDialogIfNeeded, message: " + message);
final String yes = intent.getStringExtra(EXTRA_DIALOG_YES);
LOGD(TAG, "showAnnouncementDialogIfNeeded, yes: " + yes);
final String no = intent.getStringExtra(EXTRA_DIALOG_NO);
LOGD(TAG, "showAnnouncementDialogIfNeeded, no: " + no);
final String url = intent.getStringExtra(EXTRA_DIALOG_URL);
LOGD(TAG, "showAnnouncementDialogIfNeeded, url: " + url);
final SpannableString spannable = new SpannableString(message == null ? "" : message);
Linkify.addLinks(spannable, Linkify.WEB_URLS);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
if (!TextUtils.isEmpty(title)) {
builder.setTitle(title);
}
builder.setMessage(spannable);
if (!TextUtils.isEmpty(no)) {
builder.setNegativeButton(no, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
});
}
if (!TextUtils.isEmpty(yes)) {
builder.setPositiveButton(yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(intent);
}
});
}
final AlertDialog dialog = builder.create();
dialog.show();
final TextView messageView = (TextView) dialog.findViewById(android.R.id.message);
if (messageView != null) {
// makes the embedded links in the text clickable, if there are any
messageView.setMovementMethod(LinkMovementMethod.getInstance());
}
mShowedAnnouncementDialog = true;
}
}
@Override
public void onSingleDayFragmentAttached(MyScheduleSingleDayFragment fragment) {
mMyScheduleSingleDayFragments.add(fragment);
}
@Override
public void onSingleDayFragmentDetached(MyScheduleSingleDayFragment fragment) {
mMyScheduleSingleDayFragments.remove(fragment);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.my_schedule, menu);
return true;
}
private Runnable mUpdateUIRunnable = new Runnable() {
@Override
public void run() {
MyScheduleActivity activity = MyScheduleActivity.this;
if (activity.hasBeenDestroyed()) {
LOGD(TAG, "Activity is not valid anymore. Stopping UI Updater");
return;
}
LOGD(TAG, "Running MySchedule UI updater (now=" +
new Date(TimeUtils.getCurrentTime(activity)) + ")");
mPresenter.onUserAction(MyScheduleModel.MyScheduleUserActionEnum.REDRAW_UI, null);
if (mConferenceInProgress) {
scheduleNextUIUpdate();
}
}
};
private Handler mUpdateUIHandler = new Handler();
private void scheduleNextUIUpdate() {
// Remove existing UI update runnable, if any
mUpdateUIHandler.removeCallbacks(mUpdateUIRunnable);
// Post runnable with delay
mUpdateUIHandler.postDelayed(mUpdateUIRunnable, INTERVAL_TO_REDRAW_UI);
}
private boolean hasBeenDestroyed() {
return mDestroyed;
}
}