package be.digitalia.fosdem.activities; import android.app.Dialog; import android.app.SearchManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; import android.support.annotation.StringRes; import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import android.support.v4.content.ContextCompat; import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.SharedPreferencesCompat; import android.support.v4.os.AsyncTaskCompat; import android.support.v4.view.GravityCompat; import android.support.v4.view.MenuItemCompat; import android.support.v4.widget.DrawerLayout; import android.support.v4.widget.TextViewCompat; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.AppCompatDrawableManager; import android.support.v7.widget.SearchView; import android.support.v7.widget.Toolbar; import android.text.SpannableString; import android.text.Spanned; import android.text.format.DateUtils; import android.text.method.LinkMovementMethod; import android.text.style.ForegroundColorSpan; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.animation.AnimationUtils; import android.widget.ProgressBar; import android.widget.ScrollView; import android.widget.TextView; import android.widget.Toast; import be.digitalia.fosdem.BuildConfig; import be.digitalia.fosdem.R; import be.digitalia.fosdem.api.FosdemApi; import be.digitalia.fosdem.db.DatabaseManager; import be.digitalia.fosdem.fragments.BookmarksListFragment; import be.digitalia.fosdem.fragments.LiveFragment; import be.digitalia.fosdem.fragments.MapFragment; import be.digitalia.fosdem.fragments.PersonsListFragment; import be.digitalia.fosdem.fragments.TracksFragment; import be.digitalia.fosdem.widgets.AdapterLinearLayout; /** * Main entry point of the application. Allows to switch between section fragments and update the database. * * @author Christophe Beyls */ public class MainActivity extends AppCompatActivity { public static final String ACTION_SHORTCUT_BOOKMARKS = BuildConfig.APPLICATION_ID + ".intent.action.SHORTCUT_BOOKMARKS"; public static final String ACTION_SHORTCUT_LIVE = BuildConfig.APPLICATION_ID + ".intent.action.SHORTCUT_LIVE"; private enum Section { TRACKS(TracksFragment.class, R.string.menu_tracks, R.drawable.ic_event_grey600_24dp, true, true), BOOKMARKS(BookmarksListFragment.class, R.string.menu_bookmarks, R.drawable.ic_bookmark_grey600_24dp, false, false), LIVE(LiveFragment.class, R.string.menu_live, R.drawable.ic_play_circle_outline_grey600_24dp, true, false), SPEAKERS(PersonsListFragment.class, R.string.menu_speakers, R.drawable.ic_people_grey600_24dp, false, false), MAP(MapFragment.class, R.string.menu_map, R.drawable.ic_map_grey600_24dp, false, false); private final String fragmentClassName; private final int titleResId; private final int iconResId; private final boolean extendsAppBar; private final boolean keep; Section(Class<? extends Fragment> fragmentClass, int titleResId, int iconResId, boolean extendsAppBar, boolean keep) { this.fragmentClassName = fragmentClass.getName(); this.titleResId = titleResId; this.iconResId = iconResId; this.extendsAppBar = extendsAppBar; this.keep = keep; } public String getFragmentClassName() { return fragmentClassName; } @StringRes public int getTitleResId() { return titleResId; } @DrawableRes public int getIconResId() { return iconResId; } public boolean extendsAppBar() { return extendsAppBar; } public boolean shouldKeep() { return keep; } } private static final long DATABASE_VALIDITY_DURATION = DateUtils.DAY_IN_MILLIS; private static final long DOWNLOAD_REMINDER_SNOOZE_DURATION = DateUtils.DAY_IN_MILLIS; private static final String PREF_LAST_DOWNLOAD_REMINDER_TIME = "last_download_reminder_time"; private static final String STATE_CURRENT_SECTION = "current_section"; private static final String LAST_UPDATE_DATE_FORMAT = "d MMM yyyy kk:mm:ss"; private Toolbar toolbar; ProgressBar progressBar; // Main menu Section currentSection; int pendingMenuSection = -1; int pendingMenuFooter = -1; DrawerLayout drawerLayout; private ActionBarDrawerToggle drawerToggle; View mainMenu; private TextView lastUpdateTextView; private MainMenuAdapter menuAdapter; private MenuItem searchMenuItem; private final BroadcastReceiver scheduleDownloadProgressReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { progressBar.setIndeterminate(false); progressBar.setProgress(intent.getIntExtra(FosdemApi.EXTRA_PROGRESS, 0)); } }; private final BroadcastReceiver scheduleDownloadResultReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // Hide the progress bar with a fill and fade out animation progressBar.setIndeterminate(false); progressBar.setProgress(100); progressBar.startAnimation(AnimationUtils.loadAnimation(MainActivity.this, android.R.anim.fade_out)); progressBar.setVisibility(View.GONE); int result = intent.getIntExtra(FosdemApi.EXTRA_RESULT, FosdemApi.RESULT_ERROR); String message; switch (result) { case FosdemApi.RESULT_ERROR: message = getString(R.string.schedule_loading_error); break; case FosdemApi.RESULT_UP_TO_DATE: message = getString(R.string.events_download_up_to_date); break; case 0: message = getString(R.string.events_download_empty); break; default: message = getResources().getQuantityString(R.plurals.events_download_completed, result, result); } Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show(); } }; private final BroadcastReceiver scheduleRefreshedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { updateLastUpdateTime(); } }; public static class DownloadScheduleReminderDialogFragment extends DialogFragment { @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { return new AlertDialog.Builder(getActivity()) .setTitle(R.string.download_reminder_title) .setMessage(R.string.download_reminder_message) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { ((MainActivity) getActivity()).startDownloadSchedule(); } }).setNegativeButton(android.R.string.cancel, null) .create(); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); progressBar = (ProgressBar) findViewById(R.id.progress); // Setup drawer layout getSupportActionBar().setDisplayHomeAsUpEnabled(true); drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); drawerLayout.setDrawerShadow(ContextCompat.getDrawable(this, R.drawable.drawer_shadow), GravityCompat.START); drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.main_menu, R.string.close_menu) { @Override public void onDrawerStateChanged(int newState) { super.onDrawerStateChanged(newState); if (newState == DrawerLayout.STATE_DRAGGING) { pendingMenuSection = -1; pendingMenuFooter = -1; } } @Override public void onDrawerOpened(View drawerView) { super.onDrawerOpened(drawerView); // Make keypad navigation easier mainMenu.requestFocus(); } @Override public void onDrawerClosed(View drawerView) { super.onDrawerClosed(drawerView); if (pendingMenuSection != -1) { selectMenuSection(pendingMenuSection); pendingMenuSection = -1; } if (pendingMenuFooter != -1) { selectMenuFooter(pendingMenuFooter); pendingMenuFooter = -1; } } }; drawerToggle.setDrawerIndicatorEnabled(true); drawerLayout.addDrawerListener(drawerToggle); // Disable drawerLayout focus to allow trackball navigation. // We handle the drawer closing on back press ourselves. drawerLayout.setFocusable(false); // Setup Main menu mainMenu = findViewById(R.id.main_menu); final AdapterLinearLayout sectionsList = (AdapterLinearLayout) findViewById(R.id.sections); menuAdapter = new MainMenuAdapter(getLayoutInflater()); sectionsList.setAdapter(menuAdapter); mainMenu.findViewById(R.id.settings).setOnClickListener(menuFooterClickListener); mainMenu.findViewById(R.id.about).setOnClickListener(menuFooterClickListener); LocalBroadcastManager.getInstance(this).registerReceiver(scheduleRefreshedReceiver, new IntentFilter(DatabaseManager.ACTION_SCHEDULE_REFRESHED)); // Last update date, below the list lastUpdateTextView = (TextView) mainMenu.findViewById(R.id.last_update); updateLastUpdateTime(); // Restore current section if (savedInstanceState == null) { currentSection = Section.TRACKS; String action = getIntent().getAction(); if (action != null) { switch (action) { case ACTION_SHORTCUT_BOOKMARKS: currentSection = Section.BOOKMARKS; break; case ACTION_SHORTCUT_LIVE: currentSection = Section.LIVE; break; } } String fragmentClassName = currentSection.getFragmentClassName(); Fragment f = Fragment.instantiate(this, fragmentClassName); getSupportFragmentManager().beginTransaction().add(R.id.content, f, fragmentClassName).commit(); } else { currentSection = Section.values()[savedInstanceState.getInt(STATE_CURRENT_SECTION)]; } // Ensure the current section is visible in the menu sectionsList.post(new Runnable() { @Override public void run() { if (sectionsList.getChildCount() > currentSection.ordinal()) { ScrollView mainMenuScrollView = (ScrollView) findViewById(R.id.main_menu_scroll); int requiredScroll = sectionsList.getTop() + sectionsList.getChildAt(currentSection.ordinal()).getBottom() - mainMenuScrollView.getHeight(); mainMenuScrollView.scrollTo(0, Math.max(0, requiredScroll)); } } }); updateActionBar(); } private void updateActionBar() { setTitle(currentSection.getTitleResId()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { toolbar.setElevation(currentSection.extendsAppBar() ? 0f : getResources().getDimension(R.dimen.toolbar_elevation)); } } void updateLastUpdateTime() { long lastUpdateTime = DatabaseManager.getInstance().getLastUpdateTime(); lastUpdateTextView.setText(getString(R.string.last_update, (lastUpdateTime == -1L) ? getString(R.string.never) : android.text.format.DateFormat.format(LAST_UPDATE_DATE_FORMAT, lastUpdateTime))); } @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); drawerToggle.syncState(); } @Override public void onBackPressed() { if (drawerLayout.isDrawerOpen(mainMenu)) { drawerLayout.closeDrawer(mainMenu); } else { super.onBackPressed(); } } @Override protected void onSaveInstanceState(Bundle outState) { // Ensure no fragment transaction attempt will occur after onSaveInstanceState() pendingMenuSection = -1; pendingMenuFooter = -1; super.onSaveInstanceState(outState); outState.putInt(STATE_CURRENT_SECTION, currentSection.ordinal()); } @Override protected void onStart() { super.onStart(); // Ensure the progress bar is hidden when starting progressBar.setVisibility(View.GONE); // Monitor the schedule download LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this); lbm.registerReceiver(scheduleDownloadProgressReceiver, new IntentFilter(FosdemApi.ACTION_DOWNLOAD_SCHEDULE_PROGRESS)); lbm.registerReceiver(scheduleDownloadResultReceiver, new IntentFilter(FosdemApi.ACTION_DOWNLOAD_SCHEDULE_RESULT)); // Download reminder long now = System.currentTimeMillis(); long time = DatabaseManager.getInstance().getLastUpdateTime(); if ((time == -1L) || (time < (now - DATABASE_VALIDITY_DURATION))) { SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE); time = prefs.getLong(PREF_LAST_DOWNLOAD_REMINDER_TIME, -1L); if ((time == -1L) || (time < (now - DOWNLOAD_REMINDER_SNOOZE_DURATION))) { SharedPreferencesCompat.EditorCompat.getInstance().apply( prefs.edit().putLong(PREF_LAST_DOWNLOAD_REMINDER_TIME, now) ); FragmentManager fm = getSupportFragmentManager(); if (fm.findFragmentByTag("download_reminder") == null) { new DownloadScheduleReminderDialogFragment().show(fm, "download_reminder"); } } } } @Override protected void onStop() { if ((searchMenuItem != null) && (MenuItemCompat.isActionViewExpanded(searchMenuItem))) { MenuItemCompat.collapseActionView(searchMenuItem); } LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this); lbm.unregisterReceiver(scheduleDownloadProgressReceiver); lbm.unregisterReceiver(scheduleDownloadResultReceiver); super.onStop(); } @Override protected void onDestroy() { super.onDestroy(); LocalBroadcastManager.getInstance(this).unregisterReceiver(scheduleRefreshedReceiver); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main, menu); MenuItem searchMenuItem = menu.findItem(R.id.search); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { this.searchMenuItem = searchMenuItem; // Associate searchable configuration with the SearchView SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchMenuItem); searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); } else { // Legacy search mode for Eclair MenuItemCompat.setActionView(searchMenuItem, null); MenuItemCompat.setShowAsAction(searchMenuItem, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { // Animated refresh icon menu.findItem(R.id.refresh).setIcon(R.drawable.avd_sync_white_24dp); } return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Will close the drawer if the home button is pressed if (drawerToggle.onOptionsItemSelected(item)) { return true; } switch (item.getItemId()) { case R.id.search: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { return false; } else { // Legacy search mode for Eclair onSearchRequested(); return true; } case R.id.refresh: Drawable icon = item.getIcon(); if (icon instanceof Animatable) { // Hack: reset the icon to make sure the MenuItem will redraw itself properly item.setIcon(icon); ((Animatable) icon).start(); } startDownloadSchedule(); return true; } return false; } public void startDownloadSchedule() { // Start by displaying indeterminate progress, determinate will come later progressBar.clearAnimation(); progressBar.setIndeterminate(true); progressBar.setVisibility(View.VISIBLE); AsyncTaskCompat.executeParallel(new DownloadScheduleAsyncTask(this)); } private static class DownloadScheduleAsyncTask extends AsyncTask<Void, Void, Void> { private final Context appContext; public DownloadScheduleAsyncTask(Context context) { appContext = context.getApplicationContext(); } @Override protected Void doInBackground(Void... args) { FosdemApi.downloadSchedule(appContext); return null; } } // MAIN MENU private class MainMenuAdapter extends AdapterLinearLayout.Adapter<Section> { private final Section[] sections = Section.values(); private final LayoutInflater inflater; private final int currentSectionForegroundColor; public MainMenuAdapter(LayoutInflater inflater) { this.inflater = inflater; // Select the primary color to tint the current section TypedArray a = getTheme().obtainStyledAttributes(new int[]{R.attr.colorPrimary}); try { currentSectionForegroundColor = a.getColor(0, Color.TRANSPARENT); } finally { a.recycle(); } } @Override public int getCount() { return sections.length; } @Override public Section getItem(int position) { return sections[position]; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = inflater.inflate(R.layout.item_main_menu, parent, false); convertView.setOnClickListener(sectionClickListener); } Section section = getItem(position); convertView.setSelected(section == currentSection); TextView tv = (TextView) convertView.findViewById(R.id.section_text); SpannableString sectionTitle = new SpannableString(getString(section.getTitleResId())); Drawable sectionIcon = AppCompatDrawableManager.get().getDrawable(MainActivity.this, section.getIconResId()); if (section == currentSection) { // Special color for the current section sectionTitle.setSpan(new ForegroundColorSpan(currentSectionForegroundColor), 0, sectionTitle.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // We need to mutate the drawable before applying the ColorFilter, or else all the similar drawable instances will be tinted. sectionIcon.mutate().setColorFilter(currentSectionForegroundColor, PorterDuff.Mode.SRC_IN); } tv.setText(sectionTitle); TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(tv, sectionIcon, null, null, null); return convertView; } } final View.OnClickListener sectionClickListener = new View.OnClickListener() { @Override public void onClick(View view) { pendingMenuSection = ((ViewGroup) view.getParent()).indexOfChild(view); drawerLayout.closeDrawer(mainMenu); } }; void selectMenuSection(int position) { Section section = menuAdapter.getItem(position); if (section != currentSection) { // Switch to new section FragmentManager fm = getSupportFragmentManager(); FragmentTransaction ft = fm.beginTransaction(); Fragment f = fm.findFragmentById(R.id.content); if (f != null) { if (currentSection.shouldKeep()) { ft.detach(f); } else { ft.remove(f); } } String fragmentClassName = section.getFragmentClassName(); if (section.shouldKeep() && ((f = fm.findFragmentByTag(fragmentClassName)) != null)) { ft.attach(f); } else { f = Fragment.instantiate(MainActivity.this, fragmentClassName); ft.add(R.id.content, f, fragmentClassName); } ft.commit(); currentSection = section; updateActionBar(); menuAdapter.notifyDataSetChanged(); } } private final View.OnClickListener menuFooterClickListener = new View.OnClickListener() { @Override public void onClick(View view) { pendingMenuFooter = view.getId(); drawerLayout.closeDrawer(mainMenu); } }; void selectMenuFooter(int id) { switch (id) { case R.id.settings: startActivity(new Intent(MainActivity.this, SettingsActivity.class)); overridePendingTransition(R.anim.slide_in_right, R.anim.partial_zoom_out); break; case R.id.about: new AboutDialogFragment().show(getSupportFragmentManager(), "about"); break; } } public static class AboutDialogFragment extends DialogFragment { @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { String title = String.format("%1$s %2$s", getString(R.string.app_name), BuildConfig.VERSION_NAME); return new AlertDialog.Builder(getActivity()) .setTitle(title) .setIcon(R.mipmap.ic_launcher) .setMessage(getResources().getText(R.string.about_text)) .setPositiveButton(android.R.string.ok, null) .create(); } @Override public void onStart() { super.onStart(); // Make links clickable; must be called after the dialog is shown ((TextView) getDialog().findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance()); } } }