Java tutorial
package com.wanikani.androidnotifier; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Date; import java.util.List; import java.util.Vector; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Looper; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.content.LocalBroadcastManager; import android.util.DisplayMetrics; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.TextView; import com.wanikani.androidnotifier.db.ItemsDatabase; import com.wanikani.androidnotifier.notification.NotificationService; import com.wanikani.wklib.AuthenticationException; import com.wanikani.wklib.Connection; import com.wanikani.wklib.ExtendedLevelProgression; import com.wanikani.wklib.Item; import com.wanikani.wklib.ItemLibrary; import com.wanikani.wklib.SRSDistribution; import com.wanikani.wklib.SRSLevel; import com.wanikani.wklib.StudyQueue; import com.wanikani.wklib.UserInformation; import com.wanikani.wklib.UserLogin; /* * Copyright (c) 2013 Alberto Cuda * * This program 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. * * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. */ /** * The main activity, started when the app is launched. * This class' responsibilities are mostly related to dashboard data * retrieval, since all the core tasks are implemented at fragment level. */ public class MainActivity extends FragmentActivity implements Runnable { /** * The pager model. It also broadcasts requests to all the * tabs throught the @link Tab interface. */ public class PagerAdapter extends FragmentPagerAdapter { /// The tabs List<Tab> tabs; /** * Constructor. * @param fm the fragment manager * @param tabs the list of tabs */ public PagerAdapter(FragmentManager fm, List<Tab> tabs) { super(fm); this.tabs = tabs; } @Override public int getCount() { return tabs.size(); } @Override public Fragment getItem(int position) { return (Fragment) tabs.get(position); } @Override public CharSequence getPageTitle(int position) { Resources res; res = getResources(); return res.getString(tabs.get(position).getName()); } public void replace(Tab tab) { int i; for (i = 0; i < tabs.size(); i++) if (tabs.get(i).getClass().equals(tab)) tabs.set(i, tab); } /** * Broadcasts the spin event, which is sent when data refresh * is started or completed * @see Tab#spin(boolean) * @param enable if <code>true</code>, refresh is started */ public void spin(boolean enable) { for (Tab tab : tabs) tab.spin(enable); } /** * Broadcasts the refresh-complete event, which is sent to the * tabs, providing fresh dashboard data * @see Tab#refreshComplete(DashboardData) * @param dd dashboard data */ public void refreshComplete(DashboardData dd) { for (Tab tab : tabs) tab.refreshComplete(dd); } public void flushDatabase() { for (Tab tab : tabs) tab.flushDatabase(); } /** * Broadcasts the flush request, to clear all the tabs' caches * @see Tab#flush() * @param rtype the type of refresh * @param idx the tab currently in foreground */ public void flush(Tab.RefreshType rtype, int idx) { Tab fgtab; if (conn != null) { switch (rtype) { case FULL_EXPLICIT: case FULL_IMPLICIT: conn.flush(); /* fall through */ case MEDIUM: case LIGHT: } } fgtab = idx < 0 || idx >= tabs.size() ? null : tabs.get(idx); for (Tab tab : tabs) tab.flush(rtype, fgtab == tab); } /** * Returns the index of a tab. * @param c its contents */ public int getTabIndex(Tab.Contents c) { int i; for (i = 0; i < tabs.size(); i++) if (tabs.get(i).contains(c)) return i; return -1; } public boolean backButton() { int idx; idx = pager.getCurrentItem(); return idx >= 0 && idx < tabs.size() ? tabs.get(idx).backButton() : false; } } /** * A receiver that gets notifications on several occasions: * <ul> * <li>@link {@link SettingsActivity#ACT_CREDENTIALS}: We need this * both to update the credentials used to fill the stats * screen, and to enable notifications if the user chose so. * <li>@link {@link SettingsActivity#ACT_NOTIFY}: Enable the notification * flag * <li>@link {@link #ACTION_REFRESH}: force a refresh onto the dashboard. * <li>@link {@link #ACTION_CLEAR}: force a complete refresh (including stats) * </ul> * All the events that cause a refresh of dashboard data, will also * trigger a cache flush. */ private class Receiver extends BroadcastReceiver { /** * Handles credentials change notifications * @param ctxt local context * @param i the intent */ @Override public void onReceive(Context ctxt, Intent i) { Tab.RefreshType rtype; UserLogin ul; String action; /* This check is meant to avoid a somehow strange race condition: * if a WebReviewActivity is started by tapping on the notification (i.e. the app is * NOT active), and is closed right before starting the MainActivity, the ACTION_REFRESH * is delivered by the wrong (dying) thread. This causes a ViewRoot$CalledFromWrongThreadException * It looks like an Android bug... but I'm not so sure. At any rate this check * does no harm */ if (!Looper.getMainLooper().equals(Looper.myLooper())) return; action = i.getAction(); if (action.equals(SettingsActivity.ACT_CREDENTIALS)) { ul = new UserLogin(i.getStringExtra(SettingsActivity.E_USERKEY)); updateCredentials(); enableNotifications(i.getBooleanExtra(SettingsActivity.E_ENABLED, true)); } else if (action.equals(SettingsActivity.ACT_NOTIFY)) enableNotifications(i.getBooleanExtra(SettingsActivity.E_ENABLED, true)); else if (action.equals(ACTION_REFRESH)) { rtype = i.getBooleanExtra(E_FLUSH_CACHES, true) ? Tab.RefreshType.FULL_IMPLICIT : Tab.RefreshType.MEDIUM; refresh(rtype); } else if (action.equals(ACTION_CLEAR)) flushDatabase(); } } /** * A task that gets called whenever stats need to be refreshed. * In order to keep the GUI responsive, we do this through an AsyncTask */ private class RefreshTask extends AsyncTask<Connection, Void, DashboardData> { /// The default "turtle" avatar Bitmap defAvatar; /** * Called before starting the task, inside the activity thread. */ @Override protected void onPreExecute() { Drawable d; startRefresh(); d = getResources().getDrawable(R.drawable.default_avatar); defAvatar = ((BitmapDrawable) d).getBitmap(); } /** * Performs the real job, by using the provided * connection to obtain the study queue. * @param conn a connection to the WaniKani API site */ @Override protected DashboardData doInBackground(Connection... conn) { Connection.Meter meter; DashboardData dd; UserInformation ui; StudyQueue sq; int size; size = getResources().getDimensionPixelSize(R.dimen.m_avatar_size); meter = MeterSpec.T.DASHBOARD_REFRESH.get(MainActivity.this); try { sq = conn[0].getStudyQueue(meter); /* getUserInformation should be called after at least one * of the other calls, so we give Connection a chance * to cache its contents */ ui = conn[0].getUserInformation(meter); conn[0].resolve(meter, ui, size, defAvatar); dd = new DashboardData(ui, sq); if (dd.gravatar != null) saveAvatar(dd); else restoreAvatar(dd); } catch (IOException e) { dd = new DashboardData(e); } return dd; } /** * Called at completion of the job, inside the Activity thread. * Updates the stats and the status page. * @param dd the results encoded by * {@link #doInBackground(Connection...)} */ @Override protected void onPostExecute(DashboardData dd) { try { dd.wail(); refreshComplete(dd, true); new RefreshTaskPartII().execute(conn); } catch (AuthenticationException e) { error(R.string.status_msg_unauthorized); } catch (IOException e) { error(R.string.status_msg_error); } } } /** * A task that gets called whenever the stats need to be refreshed, part II. * This task retrieves all the data that is not needed to display the dashboard, * so it can be run after the splash screen disappears (and the startup is faster). */ private class RefreshTaskPartII extends AsyncTask<Connection, Void, DashboardData.OptionalData> { /** * Called before starting the task, inside the activity thread. */ @Override protected void onPreExecute() { /* empty */ } /** * Performs the real job, by using the provided * connection to obtain the SRS distribution and the LevelProgression. * If any operation goes wrong, the data is simply not retrieved (this * is meant to be data of lesser importance), so it should be ok anyway. * @param conn a connection to the WaniKani API site */ @Override protected DashboardData.OptionalData doInBackground(Connection... conn) { DashboardData.OptionalDataStatus srsStatus, lpStatus, ciStatus; SRSDistribution srs; ExtendedLevelProgression elp; ItemLibrary<Item> critical; int cis; try { srs = conn[0].getSRSDistribution(MeterSpec.T.DASHBOARD_REFRESH.get(MainActivity.this)); srsStatus = DashboardData.OptionalDataStatus.RETRIEVED; } catch (IOException e) { srs = null; srsStatus = DashboardData.OptionalDataStatus.FAILED; } try { elp = conn[0].getExtendedLevelProgression(MeterSpec.T.DASHBOARD_REFRESH.get(MainActivity.this)); lpStatus = DashboardData.OptionalDataStatus.RETRIEVED; } catch (IOException e) { elp = null; lpStatus = DashboardData.OptionalDataStatus.FAILED; } try { critical = conn[0].getCriticalItems(MeterSpec.T.DASHBOARD_REFRESH.get(MainActivity.this)); ciStatus = DashboardData.OptionalDataStatus.RETRIEVED; cis = critical.list.size(); } catch (IOException e) { ciStatus = DashboardData.OptionalDataStatus.FAILED; cis = 0; } return new DashboardData.OptionalData(srs, srsStatus, elp, lpStatus, cis, ciStatus); } /** * Called at completion of the job, inside the Activity thread. * Updates the stats and the status page. * @param dd the results encoded by * {@link #doInBackground(Connection...)} */ @Override protected void onPostExecute(DashboardData.OptionalData od) { /* Data may disappear when we get an unauthorized code */ if (dd != null) { dd.setOptionalData(od); refreshComplete(dd, false); } } } /** * The listener of menu-related events. We intercept the refresh request * and deliver the event to the main class */ private class MenuListener extends MenuHandler.Listener { public MenuListener() { super(MainActivity.this); } @Override public void refresh() { MainActivity.this.refresh(Tab.RefreshType.FULL_EXPLICIT); } @Override public void settingsChanged() { if (dd != null) refreshComplete(dd, false); } @Override public void importFile() { Intent intent; intent = new Intent(MainActivity.this, ImportActivity.class); intent.setAction(Intent.ACTION_DEFAULT); startActivity(intent); } } private class DBFixupListener implements DatabaseFixup.Listener { @Override public void done(boolean ok) { setDBFixup(ok ? FixupState.DONE : FixupState.FAILED); } } enum FixupState { NOT_RUNNING, RUNNING, DONE, FAILED } /** The prefix */ private static final String PREFIX = MainActivity.class + "."; /*** The avatar bitmap filename */ private static final String AVATAR_FILENAME = "avatar.png"; /** The key checked by {@link #onCreate} to make * sure that the statistics contained in the bundle are valid * @see #onCreate * @see #onSaveInstanceState */ private static final String BUNDLE_VALID = PREFIX + "bundle_valid"; /** * The key stored into the bundle to keep the items cache */ private static final String ITEMS_CACHE = PREFIX + "ITEMS_CACHE"; /** * The key stored into the bundle to keep track of the current tab */ private static final String CURRENT_TAB = PREFIX + "current_tab"; /** * The key stored into the bundle to keep track of refresh operations */ private static final String REFRESHING = PREFIX + "refreshing"; /** * The key stored into the bundle to keep track of db fixup operations */ private static final String FIXUP_STATE = PREFIX + "fixup_state"; /** The broadcast receiver that handles all the actions */ private Receiver receiver; /** The object that implements the WaniKani API client */ private Connection conn; /** The information displayed on the dashboard. It is built * from the objects returned by the WaniKani API*/ private DashboardData dd; /** The asynchronous task that performs the actual queries */ RefreshTask rtask; /** The object that notifies us when the refresh timeout expires */ Alarm alarm; /** The pager */ LowPriorityViewPager pager; /** Pager adapter instance */ PagerAdapter pad; /** The dashboard height, in dip */ private int DASHBOARD_HEIGHT = 430; /** The dashboard-stats combo fragment */ DashboardStatsFragment dsf; /** The dashboard fragment */ DashboardFragment dashboardf; /** The items fragment */ ItemsFragment itemsf; /** The stats fragment */ StatsFragment statsf; /** The menu handler */ MenuHandler mh; /** Is this activity visible? */ boolean visible; /** Current layout */ SettingsActivity.Layout layout; /** Was the last instance of this activity refreshing before being destroyed? */ boolean resumeRefresh; /** Are we fixing the db up? */ FixupState dbfixup; /** An action that should be invoked to force refresh. This is used typically * when reviews complete */ public static final String ACTION_REFRESH = PREFIX + "REFRESH"; /** An action that should be invoked when the history DB is changed */ public static final String ACTION_CLEAR = PREFIX + "CLEAR"; /** * Extra parameter to {@link #ACTION_REFRESH}, to tell if caches should be flushed * or not. */ public static final String E_FLUSH_CACHES = PREFIX + "flushCaches"; /** * Called when the activity is first created. We register the * listeners and create the * {@link com.wanikani.wklib.Connection} object that will perform the * queries for us. In some cases (e.g. a change in the * orientation of the display), the activity is destroyed and * immediately recreated: to avoid querying the website, we * use the bundle to retains the stats. To make sure that the * bundle is actually valid (e.g. the previous instance of the * activity actually succeeded in retrieving the data), we * look for the {@link #BUNDLE_VALID} key. * @see #onSaveInstanceState() * @param bundle the bundle, or <code>null</code> */ @Override public void onCreate(Bundle bundle) { super.onCreate(bundle); receiver = new Receiver(); alarm = new Alarm(); mh = new MenuHandler(this, new MenuListener()); dbfixup = FixupState.NOT_RUNNING; /* Must be placed first, because fragments need this early */ conn = SettingsActivity.newConnection(this); conn.cache = new ItemsDatabase(this).getCache(); if (dsf == null) dsf = new DashboardStatsFragment(); if (dashboardf == null) dashboardf = new DashboardFragment(); if (itemsf == null) itemsf = new ItemsFragment(); if (statsf == null) statsf = new StatsFragment(); setContentView(R.layout.main); switchTo(R.id.f_splash); pager = (LowPriorityViewPager) findViewById(R.id.pager); pager.setMain(this); setLayout(); registerIntents(); if (!SettingsActivity.credentialsAreValid(this)) mh.settings(); if (bundle != null && bundle.containsKey(BUNDLE_VALID)) { dd = new DashboardData(bundle); pager.setCurrentItem(bundle.getInt(CURRENT_TAB)); /* -- I'm temporarily keeping this code just in case we decide to make disk caching optional -- */ /* try { conn.cache = (ItemsCache) bundle.getSerializable (ITEMS_CACHE); } catch (Throwable t) { } */ resumeRefresh = bundle.getBoolean(REFRESHING); dbfixup = (FixupState) bundle.getSerializable(FIXUP_STATE); } else pager.setCurrentItem(pad.getTabIndex(Tab.Contents.DASHBOARD), false); } /** * Wraps {@link SettingsActivity#getLayout(Context)} making sure * that {@link SettingsActivity.Layout#AUTO} is never returned. * It is translated into one of the concrete enums by looking at the size * of the device. * @return the layout */ protected SettingsActivity.Layout getLayout() { SettingsActivity.Layout ans; DisplayMetrics dm; float hdip; ans = SettingsActivity.getLayout(this); if (ans != SettingsActivity.Layout.AUTO) return ans; dm = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(dm); hdip = (Math.max(dm.heightPixels, dm.widthPixels)) / dm.density; return hdip >= DASHBOARD_HEIGHT * 1.5 ? SettingsActivity.Layout.LARGE : SettingsActivity.Layout.SMALL; } /** * Populates the adapter and enforces the layout, using the preferences. */ void setLayout() { List<Tab> tabs; tabs = new Vector<Tab>(); layout = getLayout(); switch (layout) { case AUTO: case SMALL: tabs.add(statsf); tabs.add(dashboardf); tabs.add(itemsf); break; case LARGE: tabs.add(dsf); tabs.add(itemsf); } pad = new PagerAdapter(getSupportFragmentManager(), tabs); pager.setAdapter(pad); } /** * Called by each tab to register itself to the activity and get called * when something interesting happens * @param tab the tab */ void register(Tab tab) { if (pad != null) pad.replace(tab); if (tab instanceof DashboardStatsFragment) dsf = (DashboardStatsFragment) tab; else if (tab instanceof DashboardFragment) dashboardf = (DashboardFragment) tab; else if (tab instanceof ItemsFragment) itemsf = (ItemsFragment) tab; else if (tab instanceof StatsFragment) statsf = (StatsFragment) tab; } @Override public void onStart() { super.onStart(); DashboardData ldd; /* A refresh task may be going on, if the review activity * has sent a refresh request before disappearing */ if (rtask == null) { ldd = dd; dd = null; if (ldd != null) { refreshComplete(ldd, false); if (resumeRefresh) refresh(Tab.RefreshType.LIGHT); else if (ldd.isIncomplete()) refreshOptional(); } else { ldd = DashboardData.fromPreferences(this, DashboardData.Source.MAIN_ACTIVITY); if (ldd == null || ldd.nextReviewDate == null || ldd.nextReviewDate.before(new Date())) ldd = DashboardData.fromPreferences(this, DashboardData.Source.NOTIFICATION_SERVICE); if (ldd == null || ldd.nextReviewDate == null || ldd.nextReviewDate.before(new Date())) ; else refreshComplete(ldd, false); refresh(Tab.RefreshType.FULL_IMPLICIT); } } resumeRefresh = false; } /** * The OS calls this method when destroying the view. * This implementation stores the stats into the bundle, if they * have been successfully retrieved. * @see #onCreate(Bundle) * @param bundle the bundle where to store the stats */ @Override public void onSaveInstanceState(Bundle bundle) { super.onSaveInstanceState(bundle); if (dd != null) { dd.serialize(bundle); bundle.putBoolean(BUNDLE_VALID, true); /* -- Needed only if we decide to provide memory cache too if (conn != null) bundle.putSerializable (ITEMS_CACHE, conn.cache); */ bundle.putInt(CURRENT_TAB, pager.getCurrentItem()); bundle.putBoolean(REFRESHING, rtask != null); bundle.putSerializable(FIXUP_STATE, dbfixup); } } /** * Called when the application resumes. * We notify this the alarm to adjust its timers in case * they were tainted by a deep sleep mode transition. */ @Override public void onResume() { super.onResume(); alarm.screenOn(); visible = true; if (layout != getLayout()) reboot(); } /** * Restarts the activity */ private void reboot() { Intent i; i = new Intent(this, getClass()); i.setAction(Intent.ACTION_MAIN); startActivity(i); finish(); } /** * Called when the application pauses. * Update the @link #visible flag. */ @Override public void onPause() { super.onPause(); visible = false; } /** * Called on dashboard destruction. * Destroys the listeners of the changes in the settings, and the * refresher thread. */ @Override public void onDestroy() { super.onDestroy(); unregisterIntents(); alarm.stopAlarm(); mh.unregister(this); } /** * Registers the intent listeners. * Currently the intents we listen to are: * <ul> * <li>{@link SettingsActivity#ACT_CREDENTIALS}, when the credentials are changed * <li>{@link SettingsActivity#ACT_NOTIFY}, when notifications are enabled or disabled * <li>{@link #ACTION_REFRESH}, when the dashboard should refreshed * </ul> * Both intents are triggered by {@link SettingsActivity} */ private void registerIntents() { IntentFilter filter; LocalBroadcastManager lbm; lbm = LocalBroadcastManager.getInstance(this); filter = new IntentFilter(SettingsActivity.ACT_CREDENTIALS); filter.addAction(SettingsActivity.ACT_NOTIFY); filter.addAction(ACTION_REFRESH); filter.addAction(ACTION_CLEAR); lbm.registerReceiver(receiver, filter); filter = new IntentFilter(ACTION_REFRESH); registerReceiver(receiver, filter); } /** * Unregister the intent listeners that were registered by {@link #registerIntent}. */ private void unregisterIntents() { LocalBroadcastManager lbm; lbm = LocalBroadcastManager.getInstance(this); lbm.unregisterReceiver(receiver); unregisterReceiver(receiver); } /** * Associates the menu description to the menu key (or action bar). * The XML description is <code>main.xml</code> */ @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main, menu); return true; } /** * Menu handler. Relays the call to the common {@link MenuHandler}. * @param item the selected menu item */ @Override public boolean onOptionsItemSelected(MenuItem item) { return mh.onOptionsItemSelected(item) || super.onOptionsItemSelected(item); } /** * Called to update the credentials. It also triggers a refresh of * the GUI. * @param login the new credentials */ private void updateCredentials() { conn = SettingsActivity.newConnection(this); conn.cache = new ItemsDatabase(this).getCache(); refresh(Tab.RefreshType.FULL_IMPLICIT); } /** * Called when the refresh timeout expires. * We call {@link #refresh()}, but we keep caches. */ public void run() { refresh(Tab.RefreshType.LIGHT); } /** * Called when the GUI needs to be refreshed. * It starts an asynchrous task that actually performs the job and * optionally clears the cache any fragment may hold. * @param rtype the type of refresh */ private void refresh(Tab.RefreshType rtype) { if (rtask != null) rtask.cancel(false); pad.flush(rtype, pager.getCurrentItem()); rtask = new RefreshTask(); rtask.execute(conn); } /** * Called when optional data needs to be refreshed. This happens in * some strange situations, e.g. when the application is stopped before * optional data, and then is resumed from a bundle. I'm not even * sure it can happen, however... */ private void refreshOptional() { new RefreshTaskPartII().execute(conn); } /** * Stores the avatar locally. Needed to avoid storing it into the * bundle. Useful also to have a fallback when we can't reach the * server. * @param dd the data, containing a valid avatar bitmap. If null, this * method does nothing */ protected void saveAvatar(DashboardData dd) { OutputStream os; if (dd == null || dd.gravatar == null) return; os = null; try { os = openFileOutput(AVATAR_FILENAME, Context.MODE_PRIVATE); dd.gravatar.compress(Bitmap.CompressFormat.PNG, 90, os); } catch (IOException e) { /* Life goes on... */ } finally { try { if (os != null) os.close(); } catch (IOException e) { /* Probably next decode will go wrong */ } } } /** * Restores the avatar from local storage. * @param dd the data, that will be filled with the bitmap, if everything * goes fine */ protected void restoreAvatar(DashboardData dd) { InputStream is; if (dd == null) return; is = null; try { is = openFileInput(AVATAR_FILENAME); dd.gravatar = BitmapFactory.decodeStream(is); } catch (IOException e) { /* Life goes on... */ } finally { try { if (is != null) is.close(); } catch (IOException e) { /* At least we tried */ } } } /** * Schedules a new refresh. Should be called right after * a refresh operation completes */ protected void reschedule() { long delay, refresh; /* Should not happen, because rescheduling makes sense only * when at least one successful retrieval completes. However...*/ if (dd == null) return; refresh = SettingsActivity.getRefreshTimeout(this) * 60 * 1000; if (dd.nextReviewDate != null) delay = dd.nextReviewDate.getTime() - System.currentTimeMillis(); else delay = refresh; if (delay > refresh || dd.reviewsAvailable > 0) delay = refresh; /* May happen if local clock is not perfectly synchronized with WK clock */ if (delay < 1000) delay = 10000; alarm.schedule(this, delay); } /** * Called by {@link RefreshTask} when asynchronous data * retrieval is completed. * @param dd the retrieved data * @param intermediate if this is an updated, and more will come */ private void refreshComplete(DashboardData dd, boolean intermediate) { if (this.dd == null) switchTo(R.id.f_main); else dd.merge(this.dd); if (!intermediate) { dd.serialize(this, DashboardData.Source.MAIN_ACTIVITY); pad.spin(false); } this.dd = dd; shareData(dd); rtask = null; if (dd.gravatar == null) restoreAvatar(dd); pad.refreshComplete(dd); reschedule(); } /** * Called when notifications are enabled or disabled. * It actually starts or terminates the {@link NotificationService}. * @param enable <code>true</code> if should be started; <code>false</code> * if should be stopped */ private void enableNotifications(boolean enable) { Intent intent; if (enable) { intent = new Intent(this, NotificationService.class); intent.setAction(NotificationService.ACTION_BOOT_COMPLETED); startService(intent); } } /** * Sends fresh dashboard data to the notification service. * @param dd the data */ private void shareData(DashboardData dd) { Intent intent; Bundle b; /* If the current activity is not visible, leave the notification service * decide whether to update data or not (there's some code in it to avoid * displaying the icon while reviews are going on, and this call would * break it) */ if (visible && dd != null) { intent = new Intent(this, NotificationService.class); intent.setAction(NotificationService.ACTION_NEW_DATA); b = new Bundle(); dd.serialize(b); intent.putExtra(NotificationService.KEY_DD, b); startService(intent); } } /** * Called when retrieveing data from WaniKani. Updates the * status message or switches to the splash screen, depending * on the current state */ private void startRefresh() { if (dd == null) switchTo(R.id.f_splash); else pad.spin(true); } /** * Displays an error message, choosing the best way to do that. * If we did not gather any stats, the only thing we can do is to display * the error page and hope for the best * @param id resource string ID */ private void error(int id) { Resources res; String s; TextView tw; /* If the API code is invalid, switch to error screen even if we have * already opened the dashboard */ if (id == R.string.status_msg_unauthorized) dd = null; if (dd == null) switchTo(R.id.f_error); else { pad.spin(false); /* Publish old version. Stats fragment needs this to make graphs "slide" */ pad.refreshComplete(dd); reschedule(); } res = getResources(); if (id == R.string.status_msg_unauthorized) s = SettingsActivity.diagnose(this, res); else s = res.getString(id); tw = (TextView) findViewById(R.id.tv_alert); if (tw != null) tw.setText(s); } /** * Called when the "Review" button is clicked. According * to user preferences, it sill start either an integrated WebView * or an external browser. */ public void review() { Intent intent; intent = new Intent(MainActivity.this, NotificationService.class); intent.setAction(NotificationService.ACTION_HIDE_NOTIFICATION); startService(intent); open(SettingsActivity.getURL(this)); } /** * Called when the "Unlock" button is clicked. According * to user preferences, it sill start either an integrated WebView * or an external browser. */ public void lessons() { open(SettingsActivity.getLessonURL(this)); } /** * Called to open an item page. * @param url the url page */ public void item(String url) { open(url); } /** * Called to open the forum page. */ public void chat() { open(SettingsActivity.getChatURL(this)); } /** * Called to open the review summary page. */ public void reviewSummary() { open(SettingsActivity.getReviewSummaryURL(this)); } /** * Open an URL. Depending on the integrated browser key, it chooses * whether to use the internal or the external browser. * @param url the URL to open */ protected void open(String url) { Intent intent; if (SettingsActivity.getUseIntegratedBrowser(this)) { intent = SettingsActivity.getWebViewIntent(MainActivity.this); intent.setAction(WebReviewActivity.OPEN_ACTION); } else intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(url)); startActivity(intent); } /** * Shows the items tab, and applies a filter to displays only the apprentice items * of a given kind. Needed by the dashboard to implement the "remaining items" * feature * @param type the type to display. Theorically it could be null, but * do we really want to? */ public void showRemaining(Item.Type type) { pager.setCurrentItem(pad.getTabIndex(Tab.Contents.ITEMS), true); itemsf.setLevelFilter(dd.level); itemsf.showSearchDialog(true, SRSLevel.APPRENTICE, type); } /** * Shows the items tab, and applies a filter to displays only the items of a specific type. * Needed by the dashboard to implement the "total items" feature * @param type the type to display. Theorically it could be null, but * do we really want to? */ public void showTotal(Item.Type type) { pager.setCurrentItem(pad.getTabIndex(Tab.Contents.ITEMS), true); itemsf.setLevelFilter(dd.level); itemsf.showSearchDialog(true, null, type, false, false); } /** * Shows the items tab, and applies a filter to displays only the critical items. * Needed by the dashboard to implement the "critical items" feature */ public void showCritical() { pager.setCurrentItem(pad.getTabIndex(Tab.Contents.ITEMS), true); itemsf.setFilter(ItemsFragment.FilterType.CRITICAL); itemsf.hideSearchDialog(); } public void showThisLevel(Item.Type type, SRSLevel srs, boolean invert) { pager.setCurrentItem(pad.getTabIndex(Tab.Contents.ITEMS), true); itemsf.setLevelFilter(dd.level); itemsf.showSearchDialog(true, srs, type, true, invert); } /** * Shows the items tab, and shows the search dialog */ public void showSearch() { int idx; idx = pad.getTabIndex(Tab.Contents.ITEMS); if (pager.getCurrentItem() != idx) { pager.setCurrentItem(pad.getTabIndex(Tab.Contents.ITEMS), true); itemsf.showSearchDialog(true, null, null); } else itemsf.showSearchDialog(false, null, null); } /** * Returns the latest version of the dashboard data. Used by tabs. * @return the dashboard data */ public DashboardData getDashboardData() { return dd; } /** * Returns the WKLib connection * @return the connection */ public Connection getConnection() { return conn; } /** * Tells whether if a given tab is intercepting scroll events * @return true if it does */ public boolean scrollLock(int item) { return pad.tabs.get(item).scrollLock(); } /** * Updates the splash screen with the version ID * @param view the view */ private void setVersion(View view) { PackageManager pmgr; PackageInfo pinfo; Resources res; TextView tv; pmgr = getPackageManager(); res = getResources(); try { pinfo = pmgr.getPackageInfo(getPackageName(), 0); tv = (TextView) view.findViewById(R.id.splash_version); tv.setText(res.getString(R.string.tag_version, pinfo.versionName)); } catch (NameNotFoundException e) { /* leave it as it is */ } } /** * Switches to one of the three screeens (main, dashboard or error) * @param id the id to switch to */ public void switchTo(int id) { View view; view = findViewById(R.id.f_main); view.setVisibility(id == R.id.f_main ? View.VISIBLE : View.INVISIBLE); view = findViewById(R.id.f_splash); setVersion(view); view.setVisibility(id == R.id.f_splash ? View.VISIBLE : View.INVISIBLE); view = findViewById(R.id.f_error); view.setVisibility(id == R.id.f_error ? View.VISIBLE : View.INVISIBLE); } @Override public boolean onSearchRequested() { showSearch(); return true; } @Override public void onBackPressed() { if (!pad.backButton()) super.onBackPressed(); } public void dbFixup() { setDBFixup(FixupState.RUNNING); new DatabaseFixup(this, conn).asyncRun(new DBFixupListener()); } private void setDBFixup(FixupState dbfixup) { this.dbfixup = dbfixup; if (statsf != null && statsf.isResumed()) statsf.setFixup(dbfixup); } private void flushDatabase() { pad.flushDatabase(); } }