/** * Copyright (C) 2013 Johannes Schnatterer * * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This file is part of nusic. * * nusic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * nusic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with nusic. If not, see <http://www.gnu.org/licenses/>. */ package info.schnatterer.nusic.android.activities; import info.schnatterer.nusic.Constants.Loaders; import info.schnatterer.nusic.android.LoadNewRelasesServiceBinding; import info.schnatterer.nusic.android.application.NusicApplication; import info.schnatterer.nusic.android.fragments.ReleaseListFragment; import info.schnatterer.nusic.android.service.LoadNewReleasesService; import info.schnatterer.nusic.android.util.TextUtil; import info.schnatterer.nusic.android.util.Toast; import info.schnatterer.nusic.ui.R; import java.util.ArrayList; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import roboguice.activity.RoboActionBarActivity; import android.Manifest; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceActivity; import android.provider.Settings; import android.support.design.widget.TabLayout; import android.support.v4.app.ActivityCompat; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.content.Loader; import android.support.v4.view.ViewPager; import android.support.v7.widget.Toolbar; import android.text.method.LinkMovementMethod; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.TextView; /** * The activity that is started when the app starts. * * The tab that is shown can parameterized using the {@link #EXTRA_ACTIVE_TAB}, * that contains a {@link TabDefinition}. * * This activity uses the toolbar as action bar, so better don't use it in * conjuntion with a theme that uses an action bar. * * @author schnatterer * */ public class MainActivity extends RoboActionBarActivity { private static final Logger LOG = LoggerFactory .getLogger(MainActivity.class); /** The request code used when starting {@link PreferenceActivity}. */ private static final int REQUEST_CODE_PREFERENCE_ACTIVITY = 0; /** * Key to the creating intent's extras that contains a {@link TabDefinition} * (as int) that can be passed to this activity within the bundle of an * intent. The corresponding value represents the Tab that is set active * with the intent. */ public static final String EXTRA_ACTIVE_TAB = "nusic.intent.extra.main.activeTab"; /** * Request permission {@link Manifest.permission#READ_EXTERNAL_STORAGE} and * call {@link #startLoadingReleasesFromInternet(boolean)} with * <code>false</code> parameter. */ private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE_FORCE_UPDATE_ONLY_IF_NECESSARY_FALSE = 0; /** * Request permission {@link Manifest.permission#READ_EXTERNAL_STORAGE} and * call {@link #startLoadingReleasesFromInternet(boolean)} with * <code>true</code> parameter. */ private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE_FORCE_UPDATE_ONLY_IF_NECESSARY_TRUE = 1; /** Start and bind the {@link LoadNewReleasesService}. */ private static LoadNewRelasesServiceBinding loadNewRelasesServiceBinding = null; /** * <code>false</code>, if {@link #onCreate(Bundle)} has never been called. * Useful for not showing the welcome screen multiple times. */ private static boolean onCreateCalled = false; /** * Stores the selected tab, even when the configuration changes. The tab * assigned here is the one that is shown when the application is started * for the first time. */ private static TabDefinition currentTab = TabDefinition.JUST_ADDED; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Set currentTab if (getIntent().hasExtra(EXTRA_ACTIVE_TAB)) { setCurrentTab(((TabDefinition) getIntent().getExtras().get( EXTRA_ACTIVE_TAB))); } // Use toolbar as actionBar, if no actionBar yet if (getSupportActionBar() == null) { Toolbar toolbar = (Toolbar) findViewById(R.id.mainToolbar); setSupportActionBar(toolbar); } ViewPager pager = (ViewPager) findViewById(R.id.mainPager); /* Init tab fragments */ TabLayout tabLayout = (TabLayout) findViewById(R.id.mainTabs); // Set adapter that handles fragment (i.e. tab creation) TabFragmentPagerAdapter tabAdapter = new TabFragmentPagerAdapter( getSupportFragmentManager()); // Create all tabs as defined for (TabDefinition tab : TabDefinition.values()) { tabAdapter.addFragment(createTabFragment(tab), getString(tab.titleId)); } pager.setAdapter(tabAdapter); // Set current tab pager.setCurrentItem(currentTab.position); tabLayout.setupWithViewPager(pager); // Handle first app start, if necessary if (!onCreateCalled) { onCreateCalled(); switch (NusicApplication.getAppStart()) { case FIRST: showWelcomeDialog(TextUtil.loadTextFromAsset(this, "welcomeDialog.html")); /* * The initialization is finished once the user dismisses the * dialog in order to avoid overlapping dialogs. */ return; case UPGRADE: showWelcomeDialog(TextUtil.loadTextFromAsset(this, "CHANGELOG.html")); /* * The initialization is finished once the user dismisses the * dialog in order to avoid overlapping dialogs. */ return; default: break; } } // Finish initialization, because no dialog was opened registerListenersAndStartLoading(false); } public static synchronized void setCurrentTab(TabDefinition currentTab) { MainActivity.currentTab = currentTab; } private static synchronized void onCreateCalled() { onCreateCalled = true; } /** * Calls {@link #registerListeners()} and also * {@link #startLoadingReleasesFromInternet(boolean)} if necessary. */ private void registerListenersAndStartLoading(boolean forceUpdate) { if (isLoadNewReleasesServiceBindingCreated()) { registerListeners(); if (forceUpdate) { requestPermissionOrStartLoadingReleasesFromInternet(false); } else { // Update only if necessary requestPermissionOrStartLoadingReleasesFromInternet(true); } } else { /* * TODO it would be best to also request the permissions here in * order to "annoy" any users that checked "Never ask again". */ registerListeners(); } } /** * Basically, this method calls * {@link #startLoadingReleasesFromInternet(boolean)}. * * Depending on the SDK version of the device requests the * {@link Manifest.permission#READ_EXTERNAL_STORAGE} permission before. * * @param updateOnlyIfNecessary */ private void requestPermissionOrStartLoadingReleasesFromInternet( boolean updateOnlyIfNecessary) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // Just call the method startLoadingReleasesFromInternet(updateOnlyIfNecessary); } else { // Do the brand new permission dance requestPermissionThenStartLoadingReleasesFromInternet(updateOnlyIfNecessary); } } /** * Calls {@link #startLoadingReleasesFromInternet(boolean)} asynchronously * via Android M's {@link #onRequestPermissionsResult(int, String[], int[])} * mechanism. * * @param updateOnlyIfNecessary */ @TargetApi(Build.VERSION_CODES.M) private void requestPermissionThenStartLoadingReleasesFromInternet( boolean updateOnlyIfNecessary) { LOG.debug("Requesting read external storage permission"); ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.READ_EXTERNAL_STORAGE }, updateOnlyIfNecessaryToRequestCode(updateOnlyIfNecessary)); } private void showDialogAccessDeniedOnce(final boolean updateOnlyIfNecessary) { new AlertDialog.Builder(this) .setTitle(R.string.MainActivity_AccessDeniedOnceTitle) .setMessage(R.string.MainActivity_AccessDeniedOnceMessage) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { // Just ask again requestPermissionThenStartLoadingReleasesFromInternet(updateOnlyIfNecessary); } }).setIcon(android.R.drawable.ic_dialog_info).show(); } private void showDialogAccessDeniedPermanently() { new AlertDialog.Builder(this) .setTitle(R.string.MainActivity_AccessDeniedPermanentlyTitle) .setMessage( R.string.MainActivity_AccessDeniedPermanentlyMessage) .setPositiveButton( R.string.MainActivity_AccessDeniedPermanentlyPositiveButton, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { Intent intent = new Intent( Settings.ACTION_APPLICATION_DETAILS_SETTINGS); Uri uri = Uri.fromParts("package", getPackageName(), null); intent.setData(uri); startActivity(intent); } }) .setNegativeButton( R.string.MainActivity_AccessDeniedPermanentlyNegativeButton, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { } }).setIcon(android.R.drawable.ic_dialog_alert).show(); } /** * "Encodes" updateOnlyIfNecessary into two different request codes * * @param updateOnlyIfNecessary * @return {@link #PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE_FORCE_UPDATE_ONLY_IF_NECESSARY_TRUE} * if <code>updateOnlyIfNecessary</code> is <code>true</code>. * Otherwise * {@link #PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE_FORCE_UPDATE_ONLY_IF_NECESSARY_FALSE} * . */ private int updateOnlyIfNecessaryToRequestCode(boolean updateOnlyIfNecessary) { if (updateOnlyIfNecessary) { return PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE_FORCE_UPDATE_ONLY_IF_NECESSARY_TRUE; } return PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE_FORCE_UPDATE_ONLY_IF_NECESSARY_FALSE; } /** * "Decodes" the different request codes to boolean updateOnlyIfNecessary * * @param requestCode * @return <code>true</code> if * {@link #PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE_FORCE_UPDATE_ONLY_IF_NECESSARY_TRUE} * is passed. Otherwise <code>false</code> */ private boolean requestCodeToUpdateOnlyIfNecessary(int requestCode) { return requestCode == PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE_FORCE_UPDATE_ONLY_IF_NECESSARY_TRUE; } @Override @TargetApi(Build.VERSION_CODES.M) public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { // Fall through, because the request code case PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE_FORCE_UPDATE_ONLY_IF_NECESSARY_FALSE: case PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE_FORCE_UPDATE_ONLY_IF_NECESSARY_TRUE: { final boolean updateOnlyIfNecessary = requestCodeToUpdateOnlyIfNecessary(requestCode); // If request is cancelled, the result arrays are empty. if (grantResults.length > 0) { if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { LOG.debug("Read external storage permission granted"); startLoadingReleasesFromInternet(updateOnlyIfNecessary); } else if (neverAskAgainCheckedForReadExternalStorage()) { /* * Never ask again selected, or device policy prohibits the * app from having that permission. */ LOG.info("Read external storage permission request permanently denied. Nusic is never going to work"); // inform the user nusic is not going to work. showDialogAccessDeniedPermanently(); } else { // Permission denied once LOG.info("Read external storage permission request denied once"); // Inform the user that nusic needs this permission showDialogAccessDeniedOnce(updateOnlyIfNecessary); } } else { LOG.warn("Read external storage permission request cancelled"); } break; } default: LOG.warn("Unexpected permission request code {}", requestCode); break; } } @TargetApi(Build.VERSION_CODES.M) private boolean neverAskAgainCheckedForReadExternalStorage() { return !ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE); } private ReleaseListFragment createTabFragment(TabDefinition tab) { // Create fragment Bundle bundle = new Bundle(); bundle.putInt(ReleaseListFragment.EXTRA_LOADER_ID, tab.loaderId); return (ReleaseListFragment) Fragment.instantiate(this, ReleaseListFragment.class.getName(), bundle); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); /* * This is needed for Android 2.x when clicking a notification an the * task is already running, the notification is not delivered, but * instead this method is called. getIntent() seems to always return the * first intent that started the app. * * For Android 4.4.2 this seems not to be called anymore. However, the * intent is delivered anyway and getIntent() always returns the last * intent that was delivered. */ if (intent.hasExtra(EXTRA_ACTIVE_TAB)) { /* Init tab fragments */ setCurrentTab((TabDefinition) intent.getExtras().get( EXTRA_ACTIVE_TAB)); ViewPager pager = (ViewPager) findViewById(R.id.mainPager); pager.setCurrentItem(currentTab.position); } } private void registerListeners() { // Set activity as new context of task loadNewRelasesServiceBinding.updateActivity(this); // loadReleasesTask.bindService(); } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.action_refresh) { startLoadingReleasesFromInternet(false); } else if (item.getItemId() == R.id.action_settings) { if (!loadNewRelasesServiceBinding.isRunning()) { startActivityForResult(new Intent(this, NusicPreferencesActivity.class), REQUEST_CODE_PREFERENCE_ACTIVITY); } else { /* * Refreshing releases is in progress, stop user from changing * service related preferences */ // TODO cancel service and restart if necessary after changing // of preferences? Toast.toast(this, R.string.MainActivity_pleaseWaitUntilRefreshIsFinished); loadNewRelasesServiceBinding.showDialog(); } } return true; } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_PREFERENCE_ACTIVITY) { // Change in preferences require a full refresh if (data.getBooleanExtra( NusicPreferencesActivity.EXTRA_RESULT_IS_REFRESH_NECESSARY, false)) { startLoadingReleasesFromInternet(false); } if (data.getBooleanExtra( NusicPreferencesActivity.EXTRA_RESULT_IS_CONTENT_CHANGED, false)) { onContentChanged(); } } } private void startLoadingReleasesFromInternet(boolean updateOnlyIfNecessary) { boolean wasRunning = !loadNewRelasesServiceBinding.refreshReleases( this, updateOnlyIfNecessary); LOG.debug("Explicit refresh triggered. Service was " + (wasRunning ? "" : " not ") + "running before"); if (wasRunning && !updateOnlyIfNecessary) { // Task is already running, just show dialog Toast.toast(this, R.string.MainActivity_refreshAlreadyInProgress); loadNewRelasesServiceBinding.showDialog(); } } @Override protected void onStart() { super.onStart(); if (loadNewRelasesServiceBinding != null) { registerListeners(); if (loadNewRelasesServiceBinding.checkDataChanged()) { onContentChanged(); } } } @Override public void onContentChanged() { super.onContentChanged(); final ViewPager pager = (ViewPager) findViewById(R.id.mainPager); final TabFragmentPagerAdapter adapter = (TabFragmentPagerAdapter) pager .getAdapter(); if (adapter != null) { runOnUiThread(new Runnable() { public void run() { for (TabDefinition tabDefinition : TabDefinition.values()) { Loader<Object> loader = getSupportLoaderManager() .getLoader(tabDefinition.loaderId); if (loader != null) { loader.onContentChanged(); } } } }); } } @Override protected void onStop() { super.onStop(); if (loadNewRelasesServiceBinding != null) { unregisterListeners(); } } @Override protected void onDestroy() { super.onDestroy(); if (loadNewRelasesServiceBinding != null) { unregisterListeners(); } } private void unregisterListeners() { loadNewRelasesServiceBinding.updateActivity(null); loadNewRelasesServiceBinding.unbindService(); } /** * Shows an alert dialog displaying some text. Useful for welcome messages. * Calls {@link #registerListenersAndStartLoading()} when the dialog is * dismissed. * * @param text * text to display. If loading from an asset, consider using * {@link TextUtil#loadTextFromAsset(android.content.Context, String)} */ @SuppressLint("InflateParams") // See http://www.doubleencore.com/2013/05/layout-inflation-as-intended/ private void showWelcomeDialog(CharSequence text) { AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this); View layout = getLayoutInflater().inflate( R.layout.simple_textview_layout, null, false); TextView textView = (TextView) layout .findViewById(R.id.renderRawHtmlTextView); textView.setText(text); textView.setMovementMethod(LinkMovementMethod.getInstance()); alertDialogBuilder .setTitle( getString(R.string.WelcomeScreenTitle, NusicApplication.getCurrentVersionName())) .setIcon(R.drawable.ic_launcher) .setOnCancelListener(new OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { registerListenersAndStartLoading(true); } }) .setPositiveButton(android.R.string.ok, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { registerListenersAndStartLoading(true); } }).setView(layout).show(); } private static synchronized boolean isLoadNewReleasesServiceBindingCreated() { if (loadNewRelasesServiceBinding == null) { loadNewRelasesServiceBinding = new LoadNewRelasesServiceBinding(); return true; } return false; } /** * Holds the basic information for each tab. * * @author schnatterer */ public static enum TabDefinition { /** First tab: Just added */ JUST_ADDED(R.string.MainActivity_TabJustAdded, Loaders.RELEASE_LOADER_JUST_ADDED), /** Second tab: Available releases */ AVAILABLE(R.string.MainActivity_TabAvailable, Loaders.RELEASE_LOADER_AVAILABLE), /** Third tab: Announced releases */ ANNOUNCED(R.string.MainActivity_TabAnnounced, Loaders.RELEASE_LOADER_ANNOUNCED), /** Fourth tab: All releases */ ALL(R.string.MainActivity_TabAll, Loaders.RELEASE_LOADER_ALL); private final int position; private final int titleId; private final int loaderId; private TabDefinition(int titleId, int loaderId) { this.position = ordinal(); this.titleId = titleId; this.loaderId = loaderId; } } /** * A fragment pager adapter that defines the content of the tabs and manages * the fragments that basically show the content of the tabs. * * @author schnatterer * */ static class TabFragmentPagerAdapter extends FragmentPagerAdapter { private final List<Fragment> mFragments = new ArrayList<>(); private final List<String> mFragmentTitles = new ArrayList<>(); public TabFragmentPagerAdapter(FragmentManager fm) { super(fm); } public void addFragment(Fragment fragment, String title) { mFragments.add(fragment); mFragmentTitles.add(title); } @Override public Fragment getItem(int position) { return mFragments.get(position); } @Override public int getCount() { return mFragments.size(); } @Override public CharSequence getPageTitle(int position) { return mFragmentTitles.get(position); } } }