Java tutorial
/* * Copyright 2013 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.gelakinetic.mtgfam; import android.app.SearchManager; import android.appwidget.AppWidgetManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.TypedArray; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.media.RingtoneManager; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; 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.view.GravityCompat; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; import com.gelakinetic.mtgfam.fragments.CardViewPagerFragment; import com.gelakinetic.mtgfam.fragments.DecklistFragment; import com.gelakinetic.mtgfam.fragments.DiceFragment; import com.gelakinetic.mtgfam.fragments.FamiliarFragment; import com.gelakinetic.mtgfam.fragments.JudgesCornerFragment; import com.gelakinetic.mtgfam.fragments.LifeCounterFragment; import com.gelakinetic.mtgfam.fragments.ManaPoolFragment; import com.gelakinetic.mtgfam.fragments.MoJhoStoFragment; import com.gelakinetic.mtgfam.fragments.PrefsFragment; import com.gelakinetic.mtgfam.fragments.ProfileFragment; import com.gelakinetic.mtgfam.fragments.ResultListFragment; import com.gelakinetic.mtgfam.fragments.RoundTimerFragment; import com.gelakinetic.mtgfam.fragments.RulesFragment; import com.gelakinetic.mtgfam.fragments.SearchViewFragment; import com.gelakinetic.mtgfam.fragments.TradeFragment; import com.gelakinetic.mtgfam.fragments.WishlistFragment; import com.gelakinetic.mtgfam.fragments.dialogs.FamiliarActivityDialogFragment; import com.gelakinetic.mtgfam.fragments.dialogs.FamiliarDialogFragment; import com.gelakinetic.mtgfam.helpers.AppIndexingWrapper; import com.gelakinetic.mtgfam.helpers.IndeterminateRefreshLayout; import com.gelakinetic.mtgfam.helpers.MTGFamiliarAppWidgetProvider; import com.gelakinetic.mtgfam.helpers.PreferenceAdapter; import com.gelakinetic.mtgfam.helpers.PriceFetchService; import com.gelakinetic.mtgfam.helpers.SearchCriteria; import com.gelakinetic.mtgfam.helpers.ToastWrapper; import com.gelakinetic.mtgfam.helpers.TutorCards; import com.gelakinetic.mtgfam.helpers.ZipUtils; import com.gelakinetic.mtgfam.helpers.database.CardDbAdapter; import com.gelakinetic.mtgfam.helpers.database.DatabaseManager; import com.gelakinetic.mtgfam.helpers.database.FamiliarDbException; import com.gelakinetic.mtgfam.helpers.lruCache.ImageCache; import com.gelakinetic.mtgfam.helpers.updaters.DbUpdaterService; import com.octo.android.robospice.SpiceManager; import org.jetbrains.annotations.NotNull; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.HttpURLConnection; import java.net.URL; import java.util.Locale; public class FamiliarActivity extends AppCompatActivity { /* Tags for fragments */ public static final String DIALOG_TAG = "dialog"; public static final String FRAGMENT_TAG = "fragment"; /* Constants used for launching activities, receiving results */ public static final String ACTION_ROUND_TIMER = "android.intent.action.ROUND_TIMER"; /* Constants used for launching fragments */ public static final String ACTION_CARD_SEARCH = "android.intent.action.CARD_SEARCH"; public static final String ACTION_LIFE = "android.intent.action.LIFE"; public static final String ACTION_DICE = "android.intent.action.DICE"; public static final String ACTION_TRADE = "android.intent.action.TRADE"; public static final String ACTION_MANA = "android.intent.action.MANA"; public static final String ACTION_WISH = "android.intent.action.WISH"; public static final String ACTION_RULES = "android.intent.action.RULES"; public static final String ACTION_JUDGE = "android.intent.action.JUDGE"; public static final String ACTION_MOJHOSTO = "android.intent.action.MOJHOSTO"; public static final String ACTION_PROFILE = "android.intent.action.PROFILE"; public static final String ACTION_DECKLIST = "android.intent.action.DECKLIST"; public static final int MY_PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 77; /* Constants used for saving state */ private static final String CURRENT_FRAG = "CURRENT_FRAG"; private static final String IS_REFRESHING = "IS_REFRESHING"; /* PayPal URL */ @SuppressWarnings("SpellCheckingInspection") public static final String PAYPAL_URL = "https://www.paypal.com/cgi-bin/webscr?cmd=_donations" + "&business=SZK4TAH2XBZNC&lc=US&item_name=MTG%20Familiar¤cy_code=USD" + "&bn=PP%2dDonationsBF%3abtn_donate_LG%2egif%3aNonHosted"; /* Timer to determine user inactivity for screen dimming in the life counter */ private static final long INACTIVITY_MS = 30000; private static final int MESSAGE_CLEAR = 0; private static final int MESSAGE_INIT_DISK_CACHE = 1; private static final int MESSAGE_FLUSH = 2; private static final int MESSAGE_CLOSE = 3; private static final String IMAGE_CACHE_DIR = "images"; /* Spice setup */ public final SpiceManager mSpiceManager = new SpiceManager(PriceFetchService.class); /* What the drawer menu will be */ private final DrawerEntry[] mPageEntries = { new DrawerEntry(R.string.main_card_search, R.attr.ic_drawer_search, false), new DrawerEntry(R.string.main_life_counter, R.attr.ic_drawer_life, false), new DrawerEntry(R.string.main_mana_pool, R.attr.ic_drawer_mana, false), new DrawerEntry(R.string.main_dice, R.attr.ic_drawer_dice, false), new DrawerEntry(R.string.main_trade, R.attr.ic_drawer_trade, false), new DrawerEntry(R.string.main_wishlist, R.attr.ic_drawer_wishlist, false), new DrawerEntry(R.string.main_decklist, R.attr.ic_drawer_deck, false), new DrawerEntry(R.string.main_timer, R.attr.ic_drawer_timer, false), new DrawerEntry(R.string.main_rules, R.attr.ic_drawer_rules, false), new DrawerEntry(R.string.main_judges_corner, R.attr.ic_drawer_judge, false), new DrawerEntry(R.string.main_mojhosto, R.attr.ic_drawer_mojhosto, false), new DrawerEntry(R.string.main_profile, R.attr.ic_drawer_profile, false), new DrawerEntry(0, 0, true), new DrawerEntry(R.string.main_settings_title, R.attr.ic_drawer_settings, false), new DrawerEntry(R.string.main_force_update_title, R.attr.ic_drawer_download, false), new DrawerEntry(R.string.main_donate_title, R.attr.ic_drawer_good, false), new DrawerEntry(R.string.main_about, R.attr.ic_drawer_about, false), new DrawerEntry(R.string.main_whats_new_title, R.attr.ic_drawer_help, false), new DrawerEntry(R.string.main_export_data_title, R.attr.ic_drawer_save, false), new DrawerEntry(R.string.main_import_data_title, R.attr.ic_drawer_load, false), }; private final Handler mInactivityHandler = new Handler(); /* Drawer elements */ public DrawerLayout mDrawerLayout; public ListView mDrawerList; public boolean mIsMenuVisible; public PreferenceAdapter mPreferenceAdapter; /* Image caching */ public ImageCache mImageCache; /* Listen for changes to preferences */ private final SharedPreferences.OnSharedPreferenceChangeListener mPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) { if (s.equals(getString(R.string.key_widgetButtons))) { Intent intent = new Intent(FamiliarActivity.this, MTGFamiliarAppWidgetProvider.class); intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); assert AppWidgetManager.getInstance(getApplication()) != null; AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getApplication()); assert appWidgetManager != null; int ids[] = appWidgetManager .getAppWidgetIds(new ComponentName(getApplication(), MTGFamiliarAppWidgetProvider.class)); intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids); sendBroadcast(intent); } else if (s.equals(getString(R.string.key_theme)) || s.equals(getString(R.string.key_language))) { /* Restart the activity for theme & language changes */ FamiliarActivity.this.finish(); startActivity( new Intent(FamiliarActivity.this, FamiliarActivity.class).setAction(Intent.ACTION_MAIN)); } else if (s.equals(getString(R.string.key_imageCacheSize))) { /* Close the old cache */ mImageCache.flush(); mImageCache.close(); /* Set up the image cache */ ImageCache.ImageCacheParams cacheParams = new ImageCache.ImageCacheParams(FamiliarActivity.this, IMAGE_CACHE_DIR); cacheParams.setMemCacheSizePercent(0.25f); // Set memory cache to 25% of app memory cacheParams.diskCacheSize = 1024 * 1024 * mPreferenceAdapter.getImageCacheSize(); addImageCache(getSupportFragmentManager(), cacheParams); } } }; public AppIndexingWrapper mAppIndexingWrapper; private ActionBarDrawerToggle mDrawerToggle; /* UI elements */ private IndeterminateRefreshLayout mRefreshLayout; /* Used to pass results between fragments */ private Bundle mFragResults; /* Timer setup */ private boolean mUpdatingRoundTimer; private long mRoundEndTime; private Handler mRoundTimerUpdateHandler; /** * This runnable is posted with a handler every second. It displays the time left in the action bar as the title * If the time runs out, it will stop updating the display and notify the fragment, if it is a RoundTimerFragment */ private final Runnable timerUpdate = new Runnable() { @Override public void run() { if (mRoundEndTime > System.currentTimeMillis()) { long timeLeftSeconds = (mRoundEndTime - System.currentTimeMillis()) / 1000; String timeLeftStr; if (timeLeftSeconds <= 0) { timeLeftStr = "00:00:00"; } else { /* This is a slight hack to handle the fact that it always rounds down. It will start the timer at 50:00 instead of 49:59, or whatever */ timeLeftSeconds++; timeLeftStr = String.format(Locale.US, "%02d:%02d:%02d", timeLeftSeconds / 3600, (timeLeftSeconds % 3600) / 60, timeLeftSeconds % 60); } if (mUpdatingRoundTimer) { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setTitle(timeLeftStr); } } mRoundTimerUpdateHandler.postDelayed(timerUpdate, 1000); } else { stopUpdatingDisplay(); Fragment fragment = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG); if (fragment instanceof RoundTimerFragment) { ((RoundTimerFragment) fragment).timerEnded(); } } } }; private int mCurrentFrag; private boolean mUserInactive = false; private final Runnable userInactive = new Runnable() { @Override public void run() { mUserInactive = true; Fragment fragment = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG); if (fragment instanceof FamiliarFragment) { ((FamiliarFragment) fragment).onUserInactive(); } } }; private DrawerEntryArrayAdapter mPagesAdapter; private final TutorCards mTutorCards = new TutorCards(this); /** * Open an inputStream to the HTML content at the given URL * * @param stringUrl The URL to open a stream to, in String form * @param logWriter A PrintWriter to log debug info to. Can be null * @return An InputStream to the content at the URL, or null * @throws IOException Thrown if something goes terribly wrong */ public static @Nullable InputStream getHttpInputStream(String stringUrl, PrintWriter logWriter) throws IOException { return getHttpInputStream(new URL(stringUrl), logWriter, 0); } /** * Open an inputStream to the HTML content at the given URL * * @param url The URL to open a stream to * @param logWriter A PrintWriter to log debug info to. Can be null * @return An InputStream to the content at the URL, or null * @throws IOException Thrown if something goes terribly wrong */ public static @Nullable InputStream getHttpInputStream(URL url, PrintWriter logWriter) throws IOException { return getHttpInputStream(url, logWriter, 0); } /** * Open an inputStream to the HTML content at the given URL, making recursive calls for * redirection (HTTP 301, 302). * * @param url The URL to open a stream to * @param logWriter A PrintWriter to log debug info to. Can be null * @param recursionLevel The redirect recursion level. Starts at 0, doesn't go past 10 * @return An InputStream to the content at the URL, or null * @throws IOException Thrown if something goes terribly wrong */ private static @Nullable InputStream getHttpInputStream(URL url, @Nullable PrintWriter logWriter, int recursionLevel) throws IOException { /* Don't allow infinite recursion */ if (recursionLevel > 10) { return null; } /* Make the URL & connection objects, follow redirects, timeout after 5s */ HttpURLConnection.setFollowRedirects(true); HttpURLConnection connection = (HttpURLConnection) (url).openConnection(); connection.setConnectTimeout(5000); connection.setInstanceFollowRedirects(true); /* If the connection is not OK, debug print the response */ if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { /* Log the URL and response code */ if (logWriter != null) { logWriter.write("URL : " + url.toString() + '\n'); logWriter.write("RESP: " + connection.getResponseCode() + '\n'); } /* Comb through header fields for a redirect location */ URL nextUrl = null; for (String key : connection.getHeaderFields().keySet()) { /* Log the header */ if (logWriter != null) { logWriter.write("HDR : [" + key + "] " + connection.getHeaderField(key) + '\n'); } /* Found the URL to try next */ if (key != null && key.equalsIgnoreCase("location")) { nextUrl = new URL(connection.getHeaderField(key)); } } /* If the next location is still null, comb through the HTML * This is kind of a hack for when sites.google.com is serving up malformed 302 * redirects and all the header fields end up being in this input stream */ if (nextUrl == null) { /* Open the stream */ BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream())); String line; int linesRead = 0; /* Read one line at a time */ while ((line = br.readLine()) != null) { /* Log the line */ if (logWriter != null) { logWriter.write("HTML:" + line + '\n'); } /* Check for a location */ if (line.toLowerCase().contains("location")) { nextUrl = new URL(line.split("\\s+")[1]); break; } /* Count the line, make sure to quit after 1000 */ linesRead++; if (linesRead > 1000) { break; } } } if (nextUrl != null) { /* If there is a URL to follow, follow it */ return getHttpInputStream(nextUrl, logWriter, recursionLevel + 1); } else { /* Otherwise return null */ return null; } } else { /* HTTP response is A-OK. Return the inputStream */ return connection.getInputStream(); } } /** * Start the Spice Manager when the activity starts */ @Override protected void onStart() { super.onStart(); mSpiceManager.start(this); } /** * Stop the Spice Manager when the activity stops */ @Override protected void onStop() { super.onStop(); mSpiceManager.shouldStop(); ToastWrapper.cancelToast(); } @Override protected void onDestroy() { super.onDestroy(); mAppIndexingWrapper.disconnectIfConnected(); mPreferenceAdapter.unregisterOnSharedPreferenceChangeListener(mPreferenceChangeListener); } /** * Called whenever we call supportInvalidateOptionsMenu(). This hides action bar items when the drawer is open * * @param menu The menu to hide or show items in * @return True if the menu is to be displayed, false otherwise */ @Override public boolean onPrepareOptionsMenu(Menu menu) { boolean drawerVisible = mDrawerLayout.isDrawerVisible(mDrawerList); mIsMenuVisible = !drawerVisible; for (int i = 0; i < menu.size(); i++) { menu.getItem(i).setVisible(!drawerVisible); } return super.onPrepareOptionsMenu(menu); } /** * Set up drawer menu * Run updater service * Check for, and potentially start, round timer * Check for TTS support * * @param savedInstanceState If the activity is being re-initialized after previously being shut down then this * Bundle contains the data it most recently supplied in onSaveInstanceState(Bundle). * Note: Otherwise it is null. */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); PrefsFragment.checkOverrideSystemLanguage(this); mPreferenceAdapter = new PreferenceAdapter(this); /* Figure out what theme the app is currently in, and change it if necessary */ int resourceId = getResourceIdFromAttr(R.attr.color_drawer_background); String themeString = ""; int otherTheme = 0; if (resourceId == R.color.drawer_background_dark) { otherTheme = R.style.Theme_light; themeString = getString(R.string.pref_theme_dark); } else if (resourceId == R.color.drawer_background_light) { otherTheme = R.style.Theme_dark; themeString = getString(R.string.pref_theme_light); } /* Switch the theme if the preference does not match the current theme */ if (!themeString.equals(mPreferenceAdapter.getTheme())) { this.setTheme(otherTheme); } /* Set the system bar color programatically, for lollipop+ */ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { getWindow().setStatusBarColor( ContextCompat.getColor(this, getResourceIdFromAttr(R.attr.colorPrimaryDark_attr))); } setContentView(R.layout.activity_main); DatabaseManager.initializeInstance(getApplicationContext()); mRefreshLayout = ((IndeterminateRefreshLayout) findViewById(R.id.fragment_container)); mRefreshLayout.setColors(ContextCompat.getColor(this, getResourceIdFromAttr(R.attr.color_common)), ContextCompat.getColor(this, getResourceIdFromAttr(R.attr.color_uncommon)), ContextCompat.getColor(this, getResourceIdFromAttr(R.attr.color_rare)), ContextCompat.getColor(this, getResourceIdFromAttr(R.attr.color_mythic))); /* Set default preferences manually so that the listener doesn't do weird things on init */ PreferenceManager.setDefaultValues(this, R.xml.preferences, false); /* Set up a listener to update the home screen widget whenever the user changes the preference */ mPreferenceAdapter.registerOnSharedPreferenceChangeListener(mPreferenceChangeListener); /* Create the handler to update the timer in the action bar */ mRoundTimerUpdateHandler = new Handler(); /* Check if we should make the timer notification */ mRoundEndTime = mPreferenceAdapter.getRoundTimerEnd(); if (mRoundEndTime != -1) { RoundTimerFragment.showTimerRunningNotification(this, mRoundEndTime); RoundTimerFragment.setOrCancelAlarms(this, mRoundEndTime, true); } mUpdatingRoundTimer = false; /* Get the drawer layout and list */ mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); mDrawerList = (ListView) findViewById(R.id.left_drawer); /* set a custom shadow that overlays the main content when the drawer opens */ mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START); /* set up the drawer's list view with items and click listener */ mPagesAdapter = new DrawerEntryArrayAdapter(this, mPageEntries); mDrawerList.setAdapter(mPagesAdapter); mDrawerList.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> adapterView, View view, int i, long l) { boolean shouldCloseDrawer = false; switch (mPageEntries[i].mNameResource) { case R.string.main_force_update_title: { if (getNetworkState(FamiliarActivity.this, true) != -1) { SQLiteDatabase database = DatabaseManager.getInstance(FamiliarActivity.this, true) .openDatabase(true); try { CardDbAdapter.dropCreateDB(database); mPreferenceAdapter.setLastLegalityUpdate(0); mPreferenceAdapter.setLastIPGUpdate(0); mPreferenceAdapter.setLastMTRUpdate(0); mPreferenceAdapter.setLastJARUpdate(0); mPreferenceAdapter.setLastRulesUpdate(0); mPreferenceAdapter.setLastUpdateTimestamp(0); mPreferenceAdapter.setLegalityTimestamp(0); startService(new Intent(FamiliarActivity.this, DbUpdaterService.class)); } catch (FamiliarDbException e) { e.printStackTrace(); } DatabaseManager.getInstance(FamiliarActivity.this, true).closeDatabase(true); } shouldCloseDrawer = true; break; } } mDrawerList.setItemChecked(mCurrentFrag, true); if (shouldCloseDrawer) { (new Handler()).postDelayed(new Runnable() { public void run() { mDrawerLayout.closeDrawer(mDrawerList); } }, 50); return true; } return false; } }); mDrawerList.setOnItemClickListener(new ListView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) { /* FamiliarFragments will automatically close the drawer when they hit onResume(). It's more precise than a delayed handler. Other options have to close the drawer themselves */ boolean shouldCloseDrawer = false; switch (mPageEntries[i].mNameResource) { case R.string.main_extras: case R.string.main_pages: { /* It's a header */ break; /* don't close the drawer or change a selection */ } case R.string.main_mana_pool: case R.string.main_dice: case R.string.main_trade: case R.string.main_wishlist: case R.string.main_decklist: case R.string.main_timer: case R.string.main_rules: case R.string.main_judges_corner: case R.string.main_mojhosto: case R.string.main_card_search: case R.string.main_life_counter: case R.string.main_profile: { selectItem(mPageEntries[i].mNameResource, null, true, false); break; } case R.string.main_settings_title: { FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); ft.addToBackStack(null); ft.replace(R.id.fragment_container, new PrefsFragment(), FamiliarActivity.FRAGMENT_TAG); ft.commit(); shouldCloseDrawer = true; break; } case R.string.main_force_update_title: { if (getNetworkState(FamiliarActivity.this, true) != -1) { mPreferenceAdapter.setLastLegalityUpdate(0); startService(new Intent(FamiliarActivity.this, DbUpdaterService.class)); } shouldCloseDrawer = true; break; } case R.string.main_donate_title: { showDialogFragment(FamiliarActivityDialogFragment.DIALOG_DONATE); shouldCloseDrawer = true; break; } case R.string.main_about: { showDialogFragment(FamiliarActivityDialogFragment.DIALOG_ABOUT); shouldCloseDrawer = true; break; } case R.string.main_whats_new_title: { showDialogFragment(FamiliarActivityDialogFragment.DIALOG_CHANGE_LOG); shouldCloseDrawer = true; break; } case R.string.main_export_data_title: { ZipUtils.exportData(FamiliarActivity.this); shouldCloseDrawer = true; break; } case R.string.main_import_data_title: { ZipUtils.importData(FamiliarActivity.this); shouldCloseDrawer = true; break; } } mDrawerList.setItemChecked(mCurrentFrag, true); if (shouldCloseDrawer) { (new Handler()).postDelayed(new Runnable() { public void run() { mDrawerLayout.closeDrawer(mDrawerList); } }, 50); } } }); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); if (toolbar != null) { //toolbar.setCollapsible(true); /* I don't like styling in java, but I can't get it to work other ways */ if (mPreferenceAdapter.getTheme().equals(getString(R.string.pref_theme_light))) { toolbar.setPopupTheme(R.style.ThemeOverlay_AppCompat_Light); } else { toolbar.setPopupTheme(R.style.ThemeOverlay_AppCompat); } toolbar.setSubtitleTextColor(ContextCompat.getColor(this, android.R.color.white)); toolbar.setTitleTextColor(ContextCompat.getColor(this, android.R.color.white)); setSupportActionBar(toolbar); } mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, toolbar, R.string.main_drawer_open, /* "open drawer" description for accessibility */ R.string.main_drawer_close /* "close drawer" description for accessibility */ ) { /** * When the drawer is opened, make sure to hide the keyboard * @param drawerView the drawer view, ignored */ @Override public void onDrawerOpened(View drawerView) { super.onDrawerOpened(drawerView); hideKeyboard(); } /** * If the menu is visible and the menu is open(ing), or the menu is not visible and the drawer is closed, * invalidate the options menu to either hide or show the action bar icons * * @param drawerView The drawer view, ignored * @param slideOffset How open the sliding menu is, between 0 and 1 */ @Override public void onDrawerSlide(View drawerView, float slideOffset) { super.onDrawerSlide(drawerView, slideOffset); if ((mIsMenuVisible && slideOffset > 0) || (slideOffset == 0 && !mIsMenuVisible)) { supportInvalidateOptionsMenu(); } } }; mDrawerLayout.setDrawerListener(mDrawerToggle); assert getSupportActionBar() != null; getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setHomeButtonEnabled(true); getSupportActionBar().setTitle(""); boolean isDeepLink = false; /* The activity can be launched a few different ways. Check the intent and show the appropriate fragment */ /* Only launch a fragment if the app isn't being recreated, i.e. savedInstanceState is null */ if (savedInstanceState == null) { isDeepLink = processIntent(getIntent()); } /* Check to see if the change log should be shown */ if (!isDeepLink) { PackageInfo pInfo; try { assert getPackageManager() != null; pInfo = getPackageManager().getPackageInfo(getPackageName(), 0); int lastVersion = mPreferenceAdapter.getLastVersion(); if (pInfo.versionCode != lastVersion) { /* Clear the spice cache on upgrade. This way, no cached values w/o foil prices will exist*/ try { mSpiceManager.removeAllDataFromCache(); } catch (NullPointerException e) { /* eat it. tasty */ } if (lastVersion != 0) { showDialogFragment(FamiliarActivityDialogFragment.DIALOG_CHANGE_LOG); } mPreferenceAdapter.setLastVersion(pInfo.versionCode); /* Clear the mtr and ipg on update, to replace them with the newly colored versions, but only if we're * updating to 3.0.1 (v24) */ if (pInfo.versionCode <= 24) { File mtr = new File(getFilesDir(), JudgesCornerFragment.MTR_LOCAL_FILE); File ipg = new File(getFilesDir(), JudgesCornerFragment.IPG_LOCAL_FILE); File jar = new File(getFilesDir(), JudgesCornerFragment.JAR_LOCAL_FILE); if (mtr.exists()) { if (!mtr.delete()) { ToastWrapper.makeText(this, mtr.getName() + " " + getString(R.string.not_deleted), ToastWrapper.LENGTH_LONG).show(); } if (!ipg.delete()) { ToastWrapper.makeText(this, ipg.getName() + " " + getString(R.string.not_deleted), ToastWrapper.LENGTH_LONG).show(); } if (!jar.delete()) { ToastWrapper.makeText(this, jar.getName() + " " + getString(R.string.not_deleted), ToastWrapper.LENGTH_LONG).show(); } } } } } catch (PackageManager.NameNotFoundException e) { /* Eat it, don't show change log */ } } /* Run the updater service if there is a network connection */ if (getNetworkState(FamiliarActivity.this, false) != -1 && mPreferenceAdapter.getAutoUpdate()) { /* Only update the banning list if it hasn't been updated recently */ long curTime = System.currentTimeMillis(); int updateFrequency = Integer.valueOf(mPreferenceAdapter.getUpdateFrequency()); int lastLegalityUpdate = mPreferenceAdapter.getLastLegalityUpdate(); /* days to ms */ if (((curTime / 1000) - lastLegalityUpdate) > (updateFrequency * 24 * 60 * 60)) { startService(new Intent(this, DbUpdaterService.class)); } } /* Set up the image cache */ ImageCache.ImageCacheParams cacheParams = new ImageCache.ImageCacheParams(this, IMAGE_CACHE_DIR); cacheParams.setMemCacheSizePercent(0.25f); // Set memory cache to 25% of app memory cacheParams.diskCacheSize = 1024 * 1024 * mPreferenceAdapter.getImageCacheSize(); addImageCache(getSupportFragmentManager(), cacheParams); /* Set up app indexing */ mAppIndexingWrapper = new AppIndexingWrapper(this); mAppIndexingWrapper.connectIfDisconnected(); } private boolean processIntent(Intent intent) { boolean isDeepLink = false; if (Intent.ACTION_SEARCH.equals(intent.getAction())) { /* Do a search by name, launched from the quick search */ String query = intent.getStringExtra(SearchManager.QUERY); Bundle args = new Bundle(); SearchCriteria sc = new SearchCriteria(); sc.name = query; args.putSerializable(SearchViewFragment.CRITERIA, sc); selectItem(R.string.main_card_search, args, false, true); /* Don't clear backstack, do force the intent */ } else if (Intent.ACTION_VIEW.equals(intent.getAction())) { boolean shouldSelectItem = true; Uri data = intent.getData(); Bundle args = new Bundle(); assert data != null; boolean shouldClearFragmentStack = true; /* Clear backstack for deep links */ if (data.getAuthority().toLowerCase().contains("gatherer.wizards")) { SQLiteDatabase database = DatabaseManager.getInstance(this, false).openDatabase(false); try { String queryParam; if ((queryParam = data.getQueryParameter("multiverseid")) != null) { Cursor cursor = CardDbAdapter.fetchCardByMultiverseId(Long.parseLong(queryParam), new String[] { CardDbAdapter.DATABASE_TABLE_CARDS + "." + CardDbAdapter.KEY_ID }, database); if (cursor.getCount() != 0) { isDeepLink = true; args.putLongArray(CardViewPagerFragment.CARD_ID_ARRAY, new long[] { cursor.getInt(cursor.getColumnIndex(CardDbAdapter.KEY_ID)) }); } cursor.close(); if (args.size() == 0) { throw new Exception("Not Found"); } } else if ((queryParam = data.getQueryParameter("name")) != null) { Cursor cursor = CardDbAdapter.fetchCardByName(queryParam, new String[] { CardDbAdapter.DATABASE_TABLE_CARDS + "." + CardDbAdapter.KEY_ID }, true, database); if (cursor.getCount() != 0) { isDeepLink = true; args.putLongArray(CardViewPagerFragment.CARD_ID_ARRAY, new long[] { cursor.getInt(cursor.getColumnIndex(CardDbAdapter.KEY_ID)) }); } cursor.close(); if (args.size() == 0) { throw new Exception("Not Found"); } } else { throw new Exception("Not Found"); } } catch (Exception e) { /* empty cursor, just return */ ToastWrapper.makeText(this, R.string.no_results_found, ToastWrapper.LENGTH_LONG).show(); this.finish(); shouldSelectItem = false; } finally { DatabaseManager.getInstance(this, false).closeDatabase(false); } } else if (data.getAuthority().contains("CardSearchProvider")) { /* User clicked a card in the quick search autocomplete, jump right to it */ args.putLongArray(CardViewPagerFragment.CARD_ID_ARRAY, new long[] { Long.parseLong(data.getLastPathSegment()) }); shouldClearFragmentStack = false; /* Don't clear backstack for search intents */ } else { /* User clicked a deep link, jump to the card(s) */ isDeepLink = true; SQLiteDatabase database = DatabaseManager.getInstance(this, false).openDatabase(false); try { Cursor cursor = null; boolean screenLaunched = false; if (data.getScheme().toLowerCase().equals("card") && data.getAuthority().toLowerCase().equals("multiverseid")) { if (data.getLastPathSegment() == null) { /* Home screen deep link */ launchHomeScreen(); screenLaunched = true; shouldSelectItem = false; } else { try { /* Don't clear the fragment stack for internal links (thanks Meld cards) */ if (data.getPathSegments().contains("internal")) { shouldClearFragmentStack = false; } cursor = CardDbAdapter.fetchCardByMultiverseId( Long.parseLong(data.getLastPathSegment()), new String[] { CardDbAdapter.DATABASE_TABLE_CARDS + "." + CardDbAdapter.KEY_ID }, database); } catch (NumberFormatException e) { cursor = null; } } } if (cursor != null) { if (cursor.getCount() != 0) { args.putLongArray(CardViewPagerFragment.CARD_ID_ARRAY, new long[] { cursor.getInt(cursor.getColumnIndex(CardDbAdapter.KEY_ID)) }); } else { /* empty cursor, just return */ ToastWrapper.makeText(this, R.string.no_results_found, ToastWrapper.LENGTH_LONG).show(); this.finish(); shouldSelectItem = false; } cursor.close(); } else if (!screenLaunched) { /* null cursor, just return */ ToastWrapper.makeText(this, R.string.no_results_found, ToastWrapper.LENGTH_LONG).show(); this.finish(); shouldSelectItem = false; } } catch (FamiliarDbException e) { e.printStackTrace(); } DatabaseManager.getInstance(this, false).closeDatabase(false); } args.putInt(CardViewPagerFragment.STARTING_CARD_POSITION, 0); if (shouldSelectItem) { selectItem(R.string.main_card_search, args, shouldClearFragmentStack, true); } } else if (ACTION_ROUND_TIMER.equals(intent.getAction())) { selectItem(R.string.main_timer, null, true, false); } else if (ACTION_CARD_SEARCH.equals(intent.getAction())) { selectItem(R.string.main_card_search, null, true, false); } else if (ACTION_LIFE.equals(intent.getAction())) { selectItem(R.string.main_life_counter, null, true, false); } else if (ACTION_DICE.equals(intent.getAction())) { selectItem(R.string.main_dice, null, true, false); } else if (ACTION_TRADE.equals(intent.getAction())) { selectItem(R.string.main_trade, null, true, false); } else if (ACTION_MANA.equals(intent.getAction())) { selectItem(R.string.main_mana_pool, null, true, false); } else if (ACTION_WISH.equals(intent.getAction())) { selectItem(R.string.main_wishlist, null, true, false); } else if (ACTION_RULES.equals(intent.getAction())) { selectItem(R.string.main_rules, null, true, false); } else if (ACTION_JUDGE.equals(intent.getAction())) { selectItem(R.string.main_judges_corner, null, true, false); } else if (ACTION_MOJHOSTO.equals(intent.getAction())) { selectItem(R.string.main_mojhosto, null, true, false); } else if (ACTION_PROFILE.equals(intent.getAction())) { selectItem(R.string.main_profile, null, true, false); } else if (ACTION_DECKLIST.equals(intent.getAction())) { selectItem(R.string.main_decklist, null, true, false); } else if (Intent.ACTION_MAIN.equals(intent.getAction())) { /* App launched as regular, show the default fragment if there isn't one already */ if (getSupportFragmentManager().getFragments() == null) { launchHomeScreen(); } } else { /* Some unknown intent, just finish */ finish(); } mDrawerList.setItemChecked(mCurrentFrag, true); return isDeepLink; } /** * Launch the home fragment, based on a preference */ private void launchHomeScreen() { String defaultFragment = mPreferenceAdapter.getDefaultFragment(); if (defaultFragment.equals(this.getString(R.string.main_card_search))) { selectItem(R.string.main_card_search, null, true, false); } else if (defaultFragment.equals(this.getString(R.string.main_life_counter))) { selectItem(R.string.main_life_counter, null, true, false); } else if (defaultFragment.equals(this.getString(R.string.main_mana_pool))) { selectItem(R.string.main_mana_pool, null, true, false); } else if (defaultFragment.equals(this.getString(R.string.main_dice))) { selectItem(R.string.main_dice, null, true, false); } else if (defaultFragment.equals(this.getString(R.string.main_trade))) { selectItem(R.string.main_trade, null, true, false); } else if (defaultFragment.equals(this.getString(R.string.main_wishlist))) { selectItem(R.string.main_wishlist, null, true, false); } else if (defaultFragment.equals(this.getString(R.string.main_timer))) { selectItem(R.string.main_timer, null, true, false); } else if (defaultFragment.equals(this.getString(R.string.main_rules))) { selectItem(R.string.main_rules, null, true, false); } else if (defaultFragment.equals(this.getString(R.string.main_judges_corner))) { selectItem(R.string.main_judges_corner, null, true, false); } else if (defaultFragment.equals(this.getString(R.string.main_mojhosto))) { selectItem(R.string.main_mojhosto, null, true, false); } else if (defaultFragment.equals(this.getString(R.string.main_profile))) { selectItem(R.string.main_profile, null, true, false); } else if (defaultFragment.equals(this.getString(R.string.main_decklist))) { selectItem(R.string.main_decklist, null, true, false); } else { selectItem(R.string.main_card_search, null, true, false); } } /** * Instead of starting a new Activity, any intents to start a new Familiar Activity will be * received here, and this Activity should react properly * * @param intent The intent used to "start" this Activity */ @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); processIntent(intent); } /** * Check to see if we should display the round timer in the actionbar, and start the inactivity timer */ @Override protected void onResume() { super.onResume(); PrefsFragment.checkOverrideSystemLanguage(this); if (mRoundEndTime != -1) { startUpdatingDisplay(); } mInactivityHandler.postDelayed(userInactive, INACTIVITY_MS); mPagesAdapter.notifyDataSetChanged(); /* To properly color icons when popping activities */ } /** * Select an item from the drawer menu. This will highlight the entry and manage fragment transactions. * * @param resId The string resource ID of the entry */ private void selectItem(int resId, Bundle args, boolean shouldClearFragmentStack, boolean forceSelect) { int position = 0; for (DrawerEntry entry : mPageEntries) { if (resId == entry.mNameResource) { break; } position++; } mCurrentFrag = position; Fragment newFrag; /* Pick the new fragment */ switch (resId) { case R.string.main_card_search: { /* If this is a quick search intent, launch either the card view or result list directly */ if (args != null && args.containsKey(CardViewPagerFragment.CARD_ID_ARRAY)) { newFrag = new CardViewPagerFragment(); } else if (args != null && args.containsKey(SearchViewFragment.CRITERIA)) { newFrag = new ResultListFragment(); } else { newFrag = new SearchViewFragment(); } break; } case R.string.main_life_counter: { newFrag = new LifeCounterFragment(); break; } case R.string.main_mana_pool: { newFrag = new ManaPoolFragment(); break; } case R.string.main_dice: { newFrag = new DiceFragment(); break; } case R.string.main_trade: { newFrag = new TradeFragment(); break; } case R.string.main_wishlist: { newFrag = new WishlistFragment(); break; } case R.string.main_decklist: { newFrag = new DecklistFragment(); break; } case R.string.main_timer: { newFrag = new RoundTimerFragment(); break; } case R.string.main_rules: { newFrag = new RulesFragment(); break; } case R.string.main_judges_corner: { newFrag = new JudgesCornerFragment(); break; } case R.string.main_mojhosto: { newFrag = new MoJhoStoFragment(); break; } case R.string.main_profile: { newFrag = new ProfileFragment(); break; } default: return; } try { if (!forceSelect && ((Object) newFrag).getClass().equals( ((Object) getSupportFragmentManager().findFragmentById(R.id.fragment_container)).getClass())) { /* This is the same fragment, just close the menu */ mDrawerLayout.closeDrawer(mDrawerList); return; } } catch (NullPointerException e) { /* no fragment to compare to */ } if (args != null) { newFrag.setArguments(args); } FragmentManager fm = getSupportFragmentManager(); FragmentTransaction ft; if (fm != null) { if (shouldClearFragmentStack) { /* Remove any current fragments on the back stack */ while (fm.getBackStackEntryCount() > 0) { fm.popBackStackImmediate(); } } /* Begin a new transaction */ ft = fm.beginTransaction(); /* Replace or add the fragment */ ft.replace(R.id.fragment_container, newFrag, FamiliarActivity.FRAGMENT_TAG); if (!shouldClearFragmentStack) { ft.addToBackStack(null); } ft.commit(); /* Color the icon when the fragment changes */ View drawerListItemView = mDrawerList.getChildAt(position); if (drawerListItemView != null) { TextView textView = (TextView) drawerListItemView.findViewById(R.id.drawer_entry_name); if (textView != null) { mPagesAdapter.colorDrawerEntry(textView); } } } } /** * mDrawerToggle.syncState() must be called during onPostCreate() * * @param savedInstanceState If the activity is being re-initialized after previously being shut down then this * Bundle contains the data it most recently supplied in onSaveInstanceState(Bundle). * Note: Otherwise it is null. */ @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); /* Sync the toggle state after onRestoreInstanceState has occurred. */ mDrawerToggle.syncState(); } /** * The drawer toggle should be notified of any configuration changes * * @param newConfig The new device configuration. */ @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); /* Pass any configuration change to the drawer toggles */ mDrawerToggle.onConfigurationChanged(newConfig); } /** * If a fragment has a result when it pops off the back stack, store it in the activity * It can be checked by the next FamiliarFragment to be displayed with getFragmentResults * The Bundle is cleared in the fragment's onResume() if it isn't accessed * * @param result The bundle of results to save */ public void setFragmentResult(Bundle result) { mFragResults = result; } /** * This will return a Bundle which a fragment has stored in the activity, and then * null the Bundle immediately * * @return a Bundle stored by the prior fragment */ public Bundle getFragmentResults() { if (mFragResults != null) { Bundle res = mFragResults; mFragResults = null; return res; } return null; } /** * Called when a key down event has occurred. Checks if the fragment should handle the key event or if the * activity should * * @param keyCode The key type for the event. We only care about KEYCODE_SEARCH * @param event description of the key event * @return True if the event was handled, false if otherwise */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_SEARCH) { Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container); /* Check to see if the current fragment did anything with the search key */ return ((FamiliarFragment) f).onInterceptSearchKey() || super.onKeyDown(keyCode, event); } /* Dinky workaround for LG phones: https://code.google.com/p/android/issues/detail?id=78154 */ else if ((keyCode == KeyEvent.KEYCODE_MENU) /* && (Build.VERSION.SDK_INT == 16) && (Build.MANUFACTURER.compareTo("LGE") == 0) */) { return true; } return super.onKeyDown(keyCode, event); } /** * Called when a key was released and not handled by any of the views inside of the activity. * So, for example, key presses while the cursor is inside a TextView will not trigger the event * (unless it is a navigation to another object) because TextView handles its own key presses. * The default implementation handles KEYCODE_BACK to stop the activity and go back. * This has a dinky workaround for LG phones * * @param keyCode The value in event.getKeyCode(). * @param event Description of the key event. * @return If you handled the event, return true. If you want to allow the event to be handled * by the next receiver, return false. */ @Override public boolean onKeyUp(int keyCode, @NotNull KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_MENU) { Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); if (toolbar != null) { if (toolbar.isOverflowMenuShowing()) { toolbar.dismissPopupMenus(); } else { toolbar.showOverflowMenu(); } } return true; } return super.onKeyUp(keyCode, event); } /** * This function hides the soft keyboard if it is being displayed. It's nice for switching fragments */ public void hideKeyboard() { try { InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); assert getCurrentFocus() != null; inputManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); } catch (NullPointerException e) { /* eat it */ } } /** * A friendly reminder to not use the regular FragmentManager, ever * * @return Return? How about an exception! */ @Override @NotNull public android.app.FragmentManager getFragmentManager() { throw new IllegalAccessError("Use .getSupportFragmentManager()"); } /** * If TTS couldn't init, show this dialog. It will only display once as to not annoy people */ public void showTtsDialog() { if (mPreferenceAdapter != null && mPreferenceAdapter.getTtsShowDialog()) { showDialogFragment(FamiliarActivityDialogFragment.DIALOG_TTS); mPreferenceAdapter.setTtsShowDialog(); } } /** * Removes a fragment with the DIALOG_TAG. The FragmentManager is a parameter so that it plays nice with nested * fragments and getChildFragmentManager() * * @param fragmentManager The FragmentManager to use for this transaction */ public void removeDialogFragment(FragmentManager fragmentManager) { if (fragmentManager != null) { Fragment prev = fragmentManager.findFragmentByTag(FamiliarActivity.DIALOG_TAG); if (prev != null) { if (prev instanceof DialogFragment) { ((DialogFragment) prev).dismiss(); } FragmentTransaction ft = fragmentManager.beginTransaction(); ft.remove(prev); ft.commit(); } } } /** * Dismiss any currently showing dialogs, then show the requested one. * * @param id the ID of the dialog to show */ private void showDialogFragment(final int id) throws IllegalStateException { /* DialogFragment.show() will take care of adding the fragment in a transaction. We also want to remove any currently showing dialog, so make our own transaction and take care of that here. */ removeDialogFragment(getSupportFragmentManager()); /* Create and show the dialog. */ FamiliarActivityDialogFragment newFragment = new FamiliarActivityDialogFragment(); Bundle arguments = new Bundle(); arguments.putInt(FamiliarDialogFragment.ID_KEY, id); newFragment.setArguments(arguments); newFragment.show(getSupportFragmentManager(), FamiliarActivity.DIALOG_TAG); } /** * Show the timer in the action bar, set up the handler to update the displayed time every second. */ public void startUpdatingDisplay() { mRoundEndTime = mPreferenceAdapter.getRoundTimerEnd(); mUpdatingRoundTimer = true; mRoundTimerUpdateHandler.removeCallbacks(timerUpdate); mRoundTimerUpdateHandler.postDelayed(timerUpdate, 1); assert getSupportActionBar() != null; getSupportActionBar().setDisplayShowTitleEnabled(true); } /** * Clear the timer from the action bar, stop the handler from updating it. */ public void stopUpdatingDisplay() { mRoundEndTime = -1; mUpdatingRoundTimer = false; mRoundTimerUpdateHandler.removeCallbacks(timerUpdate); assert getSupportActionBar() != null; getSupportActionBar().setDisplayShowTitleEnabled(false); } /** * Called when another activity returns and this one is displayed. Since the app is fragment based, this is only * called when querying for TTS support, or when returning from a ringtone picker. The preference fragment does * not handle the ringtone result correctly, so it must be caught here. * * @param requestCode The integer request code originally supplied to startActivityForResult(), allowing you to * identify who this result came from. * @param resultCode The integer result code returned by the child activity through its setResult(). * @param data An Intent, which can return result data to the caller (various data can be attached to Intent * "extras"). */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { mTutorCards.onActivityResult(requestCode, resultCode); /* The ringtone picker in the preference fragment and RoundTimerFragment will send a result here */ if (data != null && data.getExtras() != null) { if (data.getExtras().keySet().contains(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)) { Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); if (uri != null) { mPreferenceAdapter.setTimerSound(uri.toString()); } } } } /** * Handle options item presses. In this case, the home button opens and closes the drawer * * @param item The item selected * @return True if the click was acted upon, false otherwise */ @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: if (mDrawerLayout.isDrawerOpen(mDrawerList)) { mDrawerLayout.closeDrawer(mDrawerList); } else { mDrawerLayout.openDrawer(mDrawerList); } return true; default: return super.onOptionsItemSelected(item); } } /** * Called whenever the user does anything. On an interaction, reset the inactivity timer and notifies the * FamiliarFragment if it was inactive. The inactivity timer will notify the FamiliarFragment of inactivity */ @Override public void onUserInteraction() { super.onUserInteraction(); if (mUserInactive) { Fragment fragment = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG); if (fragment instanceof FamiliarFragment) { mUserInactive = true; ((FamiliarFragment) fragment).onUserActive(); } } mInactivityHandler.removeCallbacks(userInactive); mInactivityHandler.postDelayed(userInactive, INACTIVITY_MS); mUserInactive = false; } /** * Save the current fragment. * * @param outState a Bundle in which to save the state */ @Override protected void onSaveInstanceState(Bundle outState) { outState.putInt(CURRENT_FRAG, mCurrentFrag); outState.putBoolean(IS_REFRESHING, mRefreshLayout.mRefreshing); mRefreshLayout.setRefreshing(false); super.onSaveInstanceState(outState); } /** * Restore the current fragment, and highlight it * * @param savedInstanceState a Bundle which contains the saved state */ @Override protected void onRestoreInstanceState(@NotNull Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); if (savedInstanceState.containsKey(CURRENT_FRAG)) { mCurrentFrag = savedInstanceState.getInt(CURRENT_FRAG); mDrawerList.setItemChecked(mCurrentFrag, true); mRefreshLayout.setRefreshing(savedInstanceState.getBoolean(IS_REFRESHING)); } } /** * Show the indeterminate loading bar */ public void setLoading() { mRefreshLayout.setRefreshing(true); } /** * Hide the indeterminate loading bar */ public void clearLoading() { mRefreshLayout.setRefreshing(false); } /** * This helper function translates an attribute into a resource ID * * @param attr The attribute ID * @return the resource ID */ public int getResourceIdFromAttr(int attr) { assert getTheme() != null; TypedArray ta = getTheme().obtainStyledAttributes(new int[] { attr }); assert ta != null; int resId = ta.getResourceId(0, 0); ta.recycle(); return resId; } /** * Checks the networks state * * @param context the context where this is being called * @param shouldShowToast true, if you want a Toast to be shown indicating a lack of network * @return -1 if there is no network connection, or the type of network, like ConnectivityManager.TYPE_WIFI */ public static int getNetworkState(Context context, boolean shouldShowToast) { try { ConnectivityManager conMan = (ConnectivityManager) context .getSystemService(Context.CONNECTIVITY_SERVICE); for (NetworkInfo ni : conMan.getAllNetworkInfo()) { if (ni.isConnected()) { return ni.getType(); } } if (shouldShowToast) { ToastWrapper.makeText(context, R.string.no_network, ToastWrapper.LENGTH_SHORT).show(); } return -1; } catch (NullPointerException e) { if (shouldShowToast) { ToastWrapper.makeText(context, R.string.no_network, ToastWrapper.LENGTH_SHORT).show(); } return -1; } } /* * Image Caching */ private void addImageCache(FragmentManager fragmentManager, ImageCache.ImageCacheParams cacheParams) { mImageCache = ImageCache.getInstance(fragmentManager, cacheParams); new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE); } private void initDiskCacheInternal() { if (mImageCache != null) { mImageCache.initDiskCache(); } } private void clearCacheInternal() { if (mImageCache != null) { mImageCache.clearCache(); } } private void flushCacheInternal() { if (mImageCache != null) { mImageCache.flush(); } } private void closeCacheInternal() { if (mImageCache != null) { mImageCache.close(); mImageCache = null; } } /** * Callback for when a permission is requested * * @param requestCode The request code passed in requestPermissions(String[], int). * @param permissions The requested permissions. Never null. * @param grantResults The grant results for the corresponding permissions which is either * android.content.pm.PackageManager.PERMISSION_GRANTED or * android.content.pm.PackageManager.PERMISSION_DENIED. Never null. */ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); getSupportFragmentManager().findFragmentById(R.id.fragment_container) .onRequestPermissionsResult(requestCode, permissions, grantResults); } /** * Checks to see if there is network connectivity, and if there is, starts the visual * search process */ public void startTutorCardsSearch() { if (getNetworkState(FamiliarActivity.this, true) != -1) { mTutorCards.startTutorCardsSearch(); } } /** * When TutorCards returns a response over the network to a query, this function is called * with the multiverse ID of the card in the image as a parameter * * @param multiverseId The multiverse ID returned by the TutorCards query */ public void receiveTutorCardsResult(long multiverseId) { try { ((FamiliarFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_container)) .receiveTutorCardsResult(multiverseId); } catch (NullPointerException e) { /* Ignore it */ } clearLoading(); } /** * This nested class encapsulates the necessary information for an entry in the drawer menu */ public class DrawerEntry { final int mNameResource; final int mIconResource; final boolean mIsDivider; public DrawerEntry(int nameResource, int iconResource, boolean isHeader) { mNameResource = nameResource; mIconResource = iconResource; mIsDivider = isHeader; } } /** * This nested class is the adapter which populates the listView in the drawer menu. It handles both entries and * headers */ public class DrawerEntryArrayAdapter extends ArrayAdapter<DrawerEntry> { private final DrawerEntry[] values; private Drawable mHighlightedDrawable; /** * Constructor. The context will be used to inflate views later. The array of values will be used to populate * the views * * @param context The application's context, used to inflate views later. * @param values An array of DrawerEntries which will populate the list */ public DrawerEntryArrayAdapter(Context context, DrawerEntry[] values) { super(context, R.layout.drawer_list_item, values); this.values = values; } /** * Called to get a view for an entry in the listView * * @param position The position of the listView to populate * @param convertView The old view to reuse, if possible. Since the layouts for entries and headers are * different, this will be ignored * @param parent The parent this view will eventually be attached to * @return The view for the data at this position */ @NonNull @Override public View getView(int position, View convertView, @NonNull ViewGroup parent) { int layout; if (values[position].mIsDivider) { layout = R.layout.drawer_list_divider; } else { layout = R.layout.drawer_list_item; } if (convertView == null) { convertView = getLayoutInflater().inflate(layout, parent, false); } assert convertView != null; if (values[position].mIsDivider) { /* Make sure the recycled view is the right type, inflate a new one if necessary */ if (convertView.findViewById(R.id.divider) == null) { convertView = getLayoutInflater().inflate(layout, parent, false); } assert convertView != null; convertView.setFocusable(false); convertView.setFocusableInTouchMode(false); } else { /* Make sure the recycled view is the right type, inflate a new one if necessary */ if (convertView.findViewById(R.id.drawer_entry_name) == null) { convertView = getLayoutInflater().inflate(layout, parent, false); } assert convertView != null; ((TextView) convertView.findViewById(R.id.drawer_entry_name)) .setText(values[position].mNameResource); ((TextView) convertView.findViewById(R.id.drawer_entry_name)) .setCompoundDrawablesWithIntrinsicBounds( getResourceIdFromAttr(values[position].mIconResource), 0, 0, 0); /* Color the initial icon */ if (mCurrentFrag == position) { colorDrawerEntry(((TextView) convertView.findViewById(R.id.drawer_entry_name))); } else { ((TextView) convertView.findViewById(R.id.drawer_entry_name)).getCompoundDrawables()[0] .setColorFilter(null); } } return convertView; } /** * Applies the primary color to the selected icon in the drawer * * @param textView The TextView to color */ void colorDrawerEntry(TextView textView) { if (mHighlightedDrawable != null) { mHighlightedDrawable.setColorFilter(null); } mHighlightedDrawable = textView.getCompoundDrawables()[0]; mHighlightedDrawable.setColorFilter( ContextCompat.getColor(FamiliarActivity.this, getResourceIdFromAttr(R.attr.colorPrimary_attr)), PorterDuff.Mode.SRC_IN); } } private class CacheAsyncTask extends AsyncTask<Object, Void, Void> { @Override protected Void doInBackground(Object... params) { switch ((Integer) params[0]) { case MESSAGE_CLEAR: clearCacheInternal(); break; case MESSAGE_INIT_DISK_CACHE: initDiskCacheInternal(); break; case MESSAGE_FLUSH: flushCacheInternal(); break; case MESSAGE_CLOSE: closeCacheInternal(); break; } return null; } } }