package com.vaguehope.onosendai.ui;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import android.app.ActionBar;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
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.view.ColumnTitleStrip;
import android.support.v4.view.ColumnTitleStrip.ColumnClickListener;
import android.support.v4.view.ViewPager.SimpleOnPageChangeListener;
import android.util.SparseArray;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.widget.TextView;
import android.widget.Toast;
import com.vaguehope.onosendai.C;
import com.vaguehope.onosendai.R;
import com.vaguehope.onosendai.config.Account;
import com.vaguehope.onosendai.config.Column;
import com.vaguehope.onosendai.config.Config;
import com.vaguehope.onosendai.config.InternalColumnType;
import com.vaguehope.onosendai.config.Prefs;
import com.vaguehope.onosendai.images.HybridBitmapCache;
import com.vaguehope.onosendai.images.ImageLoadRequest;
import com.vaguehope.onosendai.images.ImageLoader;
import com.vaguehope.onosendai.images.ImageLoaderUtils;
import com.vaguehope.onosendai.model.ScrollState;
import com.vaguehope.onosendai.model.Tweet;
import com.vaguehope.onosendai.notifications.Notifications;
import com.vaguehope.onosendai.provider.ProviderMgr;
import com.vaguehope.onosendai.provider.ProviderMgr.ProviderMgrProvider;
import com.vaguehope.onosendai.storage.DbClient;
import com.vaguehope.onosendai.storage.DbInterface;
import com.vaguehope.onosendai.storage.DbProvider;
import com.vaguehope.onosendai.ui.LocalSearchDialog.OnTweetListener;
import com.vaguehope.onosendai.ui.pref.AdvancedPrefFragment;
import com.vaguehope.onosendai.ui.pref.FetchingPrefFragment;
import com.vaguehope.onosendai.ui.pref.FiltersPrefFragment;
import com.vaguehope.onosendai.ui.pref.OsPreferenceActivity;
import com.vaguehope.onosendai.ui.pref.UiPrefFragment;
import com.vaguehope.onosendai.update.AlarmReceiver;
import com.vaguehope.onosendai.update.BatteryNotify;
import com.vaguehope.onosendai.update.UpdateService;
import com.vaguehope.onosendai.util.BatteryHelper;
import com.vaguehope.onosendai.util.DialogHelper;
import com.vaguehope.onosendai.util.LogWrapper;
import com.vaguehope.onosendai.util.MultiplexingOnPageChangeListener;
import com.vaguehope.onosendai.util.NetHelper;
import com.vaguehope.onosendai.util.exec.ExecUtils;
import com.vaguehope.onosendai.util.exec.ExecutorEventListener;
import com.vaguehope.onosendai.util.exec.TrackingAsyncTask;
import com.vaguehope.onosendai.widget.SidebarAwareViewPager;
public class MainActivity extends FragmentActivity implements ImageLoader, DbProvider, ProviderMgrProvider, OnSharedPreferenceChangeListener {
public static final String ARG_FOCUS_COLUMN_ID = "focus_column_id";
private static final LogWrapper LOG = new LogWrapper("MA");
private Prefs prefs;
private Config conf;
private ProviderMgr providerMgr;
private HybridBitmapCache imageCache;
private ExecutorStatus executorStatus;
private ExecutorService localEs;
private ExecutorService netEs;
private MessageHandler msgHandler;
private boolean columnsRtl;
private ColumnTitleStrip columnTitleStrip;
private SidebarAwareViewPager viewPager;
private VisiblePageSelectionListener pageSelectionListener;
private final SparseArray<TweetListFragment> activePages = new SparseArray<TweetListFragment>();
private boolean prefsChanged = false;
@Override
protected void onCreate (final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.prefs = new Prefs(getBaseContext());
if (!this.prefs.isConfigured()) {
startActivity(new Intent(getApplicationContext(), SetupActivity.class));
finish();
return;
}
this.prefs.getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setContentView(R.layout.activity_main);
try {
this.conf = this.prefs.asConfig();
}
catch (final Exception e) { // No point continuing if any exception.
LOG.wtf("Unparsable config.", e);
DialogHelper.alertAndClose(this, e);
return;
}
this.imageCache = new HybridBitmapCache(this, C.MAX_MEMORY_IMAGE_CACHE);
if (this.prefs.getSharedPreferences().getBoolean(AdvancedPrefFragment.KEY_THREAD_INSPECTOR, false)) {
final TextView jobStatus = (TextView) findViewById(R.id.jobStatus);
jobStatus.setVisibility(View.VISIBLE);
this.executorStatus = new ExecutorStatus(jobStatus);
}
this.localEs = ExecUtils.newBoundedCachedThreadPool(C.LOCAL_MAX_THREADS, new LogWrapper("LES"), this.executorStatus);
this.netEs = ExecUtils.newBoundedCachedThreadPool(C.NET_MAX_THREADS, new LogWrapper("NES"), this.executorStatus);
this.msgHandler = new MessageHandler(this);
final float columnWidth = UiPrefFragment.readColumnWidth(this, this.prefs);
this.columnsRtl = UiPrefFragment.readColumnsRtl(this.prefs);
// If this becomes too memory intensive, switch to android.support.v4.app.FragmentStatePagerAdapter.
final SectionsPagerAdapter sectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager(), this, columnWidth);
this.viewPager = (SidebarAwareViewPager) findViewById(R.id.pager);
this.pageSelectionListener = new VisiblePageSelectionListener(columnWidth);
final MultiplexingOnPageChangeListener onPageChangeListener = new MultiplexingOnPageChangeListener(
this.pageSelectionListener,
new NotificationClearingPageSelectionListener(this));
this.viewPager.setOnPageChangeListener(onPageChangeListener);
this.viewPager.setAdapter(sectionsPagerAdapter);
if (!showPageFromIntent(getIntent())) {
if (this.columnsRtl) {
gotoPage(0);
}
else {
onPageChangeListener.onPageSelected(this.viewPager.getCurrentItem());
}
}
final ActionBar ab = getActionBar();
ab.setDisplayShowHomeEnabled(true);
ab.setHomeButtonEnabled(true);
ab.setDisplayShowTitleEnabled(false);
ab.setDisplayShowCustomEnabled(true);
this.columnTitleStrip = new ColumnTitleStrip(ab.getThemedContext());
this.columnTitleStrip.setViewPager(this.viewPager);
this.columnTitleStrip.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
this.columnTitleStrip.setColumnClickListener(new TitleClickListener(this));
ab.setCustomView(this.columnTitleStrip);
AlarmReceiver.configureAlarms(this);
new CheckBackgroundUpdatesRunning(this, this.executorStatus).executeOnExecutor(this.localEs);
}
@Override
protected void onNewIntent (final Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
showPageFromIntent(intent);
}
@Override
public void onResume () {
super.onResume();
resumeDb();
if (this.prefsChanged) {
finish();
startActivity(getIntent());
return;
}
startBgFetchVisibleColumnsIfConfigured();
}
@Override
protected void onDestroy () {
stopBgFetchVisibleColumns();
if (this.prefs != null) this.prefs.getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
if (this.providerMgr != null) this.providerMgr.shutdown();
if (this.imageCache != null) this.imageCache.clean();
if (this.netEs != null) this.netEs.shutdown();
if (this.localEs != null) this.localEs.shutdown();
disposeDb();
super.onDestroy();
}
@Override
public void onSharedPreferenceChanged (final SharedPreferences sharedPreferences, final String key) {
this.prefsChanged = true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
private final CountDownLatch dbReadyLatch = new CountDownLatch(1);
private DbClient bndDb;
private void resumeDb () {
if (this.bndDb == null) {
LOG.d("Binding DB service...");
final CountDownLatch latch = this.dbReadyLatch;
final LogWrapper log = LOG;
this.bndDb = new DbClient(this, LOG.getPrefix(), new Runnable() {
@Override
public void run () {
latch.countDown();
log.d("DB service bound.");
setProviderMgr(new ProviderMgr(getDb()));
}
});
}
}
private void disposeDb () {
if (this.bndDb != null) this.bndDb.dispose();
}
private boolean waitForDbReady () {
boolean dbReady = false;
try {
dbReady = this.dbReadyLatch.await(C.DB_CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
catch (final InterruptedException e) {/**/}
if (!dbReady) LOG.e("Not updateing: Time out waiting for DB service to connect.");
return dbReady;
}
@Override
public DbInterface getDb () {
final DbClient d = this.bndDb;
if (d == null) return null;
return d.getDb();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Prefs getPrefs () {
return this.prefs;
}
boolean isShowFiltered () {
return this.prefs.getSharedPreferences().getBoolean(FiltersPrefFragment.KEY_SHOW_FILTERED, false);
}
Config getConf () {
return this.conf;
}
public ExecutorEventListener getExecutorEventListener () {
return this.executorStatus;
}
public Executor getLocalEs () {
return this.localEs;
}
public Executor getNetEs () {
return this.netEs;
}
@Override
public ProviderMgr getProviderMgr () {
if (!waitForDbReady()) throw new IllegalStateException("DB not bound.");
return this.providerMgr;
}
void setProviderMgr (final ProviderMgr providerMgr) {
this.providerMgr = providerMgr;
}
public HybridBitmapCache getImageCache () {
return this.imageCache;
}
@Override
public void loadImage (final ImageLoadRequest req) {
ImageLoaderUtils.loadImage(this.imageCache, req, this.localEs, this.netEs, this.executorStatus);
}
public int convertPagePosition (final int argPosition) {
if (!this.columnsRtl) return argPosition;
final int count = this.viewPager.getAdapter().getCount();
if (count < 1) return argPosition;
return count - 1 - argPosition;
}
public boolean gotoPage (final int argPosition) {
final int position = convertPagePosition(argPosition);
if (this.viewPager.getCurrentItem() != position) {
this.viewPager.setCurrentItem(position, false);
return true;
}
return false;
}
public void gotoTweet (final int colId, final Tweet tweet) {
gotoPage(getConf().getColumnPositionById(colId));
final TweetListFragment page = getActivePageById(colId);
if (page != null) page.scrollToTweet(tweet);
}
private boolean showPageFromIntent (final Intent intent) {
if (intent.hasExtra(ARG_FOCUS_COLUMN_ID)) {
final int pos = this.conf.getColumnPositionById(intent.getIntExtra(ARG_FOCUS_COLUMN_ID, 0));
if (pos >= 0) return gotoPage(pos);
}
return false;
}
protected TweetListFragment getActivePageById (final int colId) {
return this.activePages.get(colId);
}
protected void onFragmentResumed (final int columnId, final TweetListFragment page) {
this.activePages.put(columnId, page);
}
protected void onFragmentPaused (final int columnId) {
this.activePages.remove(columnId);
}
protected ScrollState getColumnScroll (final int columnId) {
final TweetListFragment page = getActivePageById(columnId);
if (page == null) return null;
return page.getCurrentScroll();
}
@Override
public void onBackPressed () {
for (int i = 0; i < this.activePages.size(); i++) {
final TweetListFragment page = this.activePages.valueAt(i);
if (this.pageSelectionListener.isVisible(convertPagePosition(page.getColumnPosition())) && page.getSidebar().closeSidebar()) return;
}
super.onBackPressed();
}
public List<Column> getVisibleColumns () {
final List<Column> ret = new ArrayList<Column>();
for (int i = 0; i < this.conf.getColumnCount(); i++) {
if (this.pageSelectionListener.isVisible(convertPagePosition(i))) ret.add(this.conf.getColumnByPosition(i));
}
return ret;
}
public int[] getVisibleColumnIds () {
final List<Column> pages = getVisibleColumns();
final int[] ret = new int[pages.size()];
for (int i = 0; i < pages.size(); i++) {
ret[i] = pages.get(i).getId();
}
return ret;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@Override
public boolean onCreateOptionsMenu (final Menu menu) {
getMenuInflater().inflate(R.menu.listmenu, menu);
/* FIXME apparently this.pageSelectionListener can sometimes be null here.
* Reported on Galaxy S5 stock firmware v6.0.1.
*/
if (this.pageSelectionListener != null && this.pageSelectionListener.getVisiblePageCount() > 1) {
menu.findItem(R.id.mnuRefreshColumnNow).setTitle(R.string.main_menu_refresh_visible_columns);
}
return true;
}
@Override
public boolean onOptionsItemSelected (final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
new GotoMenu(this).onClick(this.columnTitleStrip); // TODO FIXME position it correctly under icon.
return true;
case R.id.mnuPost:
showPost();
return true;
case R.id.mnuOutbox:
showOutbox();
return true;
case R.id.mnuRefreshColumnNow:
scheduleRefreshInteractive(getVisibleColumnIds());
return true;
case R.id.mnuRefreshAllNow:
scheduleRefreshInteractive();
return true;
case R.id.mnuPreferences:
startActivity(new Intent(this, OsPreferenceActivity.class));
return true;
case R.id.mnuLocalSearch:
showLocalSearch();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void showPost () {
showPost(getVisibleColumns(), null, -1);
}
protected void showPost (final Collection<Column> cols, final String initialBody, final int cursorPosition) {
final Set<String> acIds = Column.uniqAccountIds(cols);
final String accountId = acIds.size() == 1 ? acIds.iterator().next() : null;
showPost(this.conf.getAccount(accountId), initialBody, cursorPosition);
}
private void showPost (final Account account, final String initialBody, final int cursorPosition) {
final Intent intent = new Intent(this, PostActivity.class);
if (account != null) intent.putExtra(PostActivity.ARG_ACCOUNT_ID, account.getId());
if (initialBody != null) intent.putExtra(PostActivity.ARG_BODY, initialBody);
if (cursorPosition >= 0) intent.putExtra(PostActivity.ARG_BODY_CURSOR_POSITION, cursorPosition);
startActivity(intent);
}
private void showOutbox () {
startActivity(new Intent(this, OutboxActivity.class));
}
/**
* return true if scheduled.
*/
protected boolean scheduleRefreshInteractive (final int... columnIds) {
if (!NetHelper.connectionPresent(this)) {
DialogHelper.alert(this, getString(R.string.main_no_internet_connection_available));
return false;
}
scheduleRefresh(true, columnIds);
return true;
}
protected void scheduleRefreshBackground (final int... columnIds) {
if (!NetHelper.connectionPresent(this)) {
Toast.makeText(this, R.string.main_no_internet_connection_available, Toast.LENGTH_SHORT).show();
return;
}
scheduleRefresh(false, columnIds);
}
private void scheduleRefresh (final boolean manual, final int... columnIds) {
final Intent intent = new Intent(this, UpdateService.class);
if (manual) intent.putExtra(UpdateService.ARG_IS_MANUAL, true);
if (columnIds != null && columnIds.length > 0) intent.putExtra(UpdateService.ARG_COLUMN_IDS, columnIds);
startService(intent);
if (manual) {
final int count = columnIds == null ? 0 : columnIds.length;
final String msg = count > 0
? String.format(getString(R.string.main_refresh_columns_requested), count)
: getString(R.string.main_refresh_all_columns_requested);
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
}
}
private void showLocalSearch () {
LocalSearchDialog.show(this, getConf(), this, this, new OnTweetListener() {
@Override
public void onTweet (final int colId, final Tweet tweet) {
MainActivity.this.gotoTweet(colId, tweet);
}
});
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
protected void setTempColumnTitle (final int argPosition, final String title) {
final int position = convertPagePosition(argPosition);
this.columnTitleStrip.setTempColumnTitle(position, title);
}
private int progressIndicatorCounter = 0;
/**
* Only call on UI thread.
*/
protected void progressIndicator (final boolean inProgress) {
this.progressIndicatorCounter += (inProgress ? 1 : -1);
setProgressBarIndeterminateVisibility(this.progressIndicatorCounter > 0);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
private enum Msgs {
BG_FETCH_VISIBLE_COLUMNS;
public static final Msgs values[] = values(); // Optimisation to avoid new array every time.
}
private static class MessageHandler extends Handler {
private final WeakReference<MainActivity> parentRef;
public MessageHandler (final MainActivity parent) {
this.parentRef = new WeakReference<MainActivity>(parent);
}
@Override
public void handleMessage (final Message msg) {
final MainActivity parent = this.parentRef.get();
if (parent != null) parent.msgOnUiThread(msg);
}
}
protected void msgOnUiThread (final Message msg) {
switch (Msgs.values[msg.what]) {
case BG_FETCH_VISIBLE_COLUMNS:
bgFetchVisibleColumns();
break;
default:
}
}
private boolean frequentlyFetchVisibleColumns = false;
private void startBgFetchVisibleColumnsIfConfigured () {
if (this.frequentlyFetchVisibleColumns) return;
this.frequentlyFetchVisibleColumns = isAnyFrequentFetchColumnsConfigured();
if (this.frequentlyFetchVisibleColumns) {
this.msgHandler.sendEmptyMessageDelayed(Msgs.BG_FETCH_VISIBLE_COLUMNS.ordinal(), TimeUnit.SECONDS.toMillis(C.FETCH_VISIBLE_INITIAL_SECONDS));
LOG.i("Frequently fetch visible colunns enabled.");
}
}
private void stopBgFetchVisibleColumns () {
this.frequentlyFetchVisibleColumns = false;
}
private void bgFetchVisibleColumns () {
if (!this.frequentlyFetchVisibleColumns) return;
final int[] visibleColumnIds = getVisibleColumnIds();
if (visibleColumnIds.length > 0) {
scheduleRefreshBackground(visibleColumnIds);
LOG.i("Requested fetch of visible colunns: %s.", Arrays.toString(visibleColumnIds));
}
else {
LOG.i("No visible colunns to refresh refresh of: %s.", Arrays.toString(visibleColumnIds));
}
this.msgHandler.sendEmptyMessageDelayed(Msgs.BG_FETCH_VISIBLE_COLUMNS.ordinal(), TimeUnit.MINUTES.toMillis(C.FETCH_VISIBLE_INTERVAL_MIN));
}
private boolean isAnyFrequentFetchColumnsConfigured () {
for (final Column col : this.conf.getColumns()) {
if (col.getRefreshIntervalMins() > 0 && col.getRefreshIntervalMins() < C.FETCH_VISIBLE_THRESHOLD_MIN) return true;
}
return false;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
private static class SectionsPagerAdapter extends FragmentPagerAdapter {
private final MainActivity host;
private final float pageWidth;
public SectionsPagerAdapter (final FragmentManager fm, final MainActivity host, final float pageWidth) {
super(fm);
this.host = host;
this.pageWidth = pageWidth;
}
@Override
public Fragment getItem (final int argPosition) {
final int position = this.host.convertPagePosition(argPosition);
final Column col = this.host.getConf().getColumnByPosition(position);
final Fragment fragment = new TweetListFragment();
final Bundle args = new Bundle();
args.putInt(TweetListFragment.ARG_COLUMN_POSITION, position);
args.putInt(TweetListFragment.ARG_COLUMN_ID, col.getId());
args.putString(TweetListFragment.ARG_COLUMN_TITLE, col.getTitle());
args.putBoolean(TweetListFragment.ARG_COLUMN_IS_LATER, InternalColumnType.LATER.matchesColumn(col));
args.putString(TweetListFragment.ARG_COLUMN_SHOW_INLINEMEDIA, col.getInlineMediaStyle() != null ? col.getInlineMediaStyle().serialise() : null);
args.putBoolean(TweetListFragment.ARG_SHOW_FILTERED, this.host.isShowFiltered());
fragment.setArguments(args);
return fragment;
}
@Override
public int getCount () {
return this.host.getConf().getColumns().size();
}
@Override
public CharSequence getPageTitle (final int argPosition) {
final int position = this.host.convertPagePosition(argPosition);
return this.host.getConf().getColumnByPosition(position).getTitle();
}
@Override
public float getPageWidth (final int position) {
return this.pageWidth;
}
}
private static class VisiblePageSelectionListener extends SimpleOnPageChangeListener {
private int selectedPagePosition;
private final int visiblePages;
public VisiblePageSelectionListener (final float pageWidth) {
this.visiblePages = (int) (1 / pageWidth);
}
@Override
public void onPageSelected (final int position) {
this.selectedPagePosition = position;
}
public int getVisiblePageCount () {
return this.visiblePages;
}
public boolean isVisible (final int position) {
return position >= this.selectedPagePosition && position < this.selectedPagePosition + this.visiblePages;
}
}
private static class TitleClickListener implements ColumnClickListener {
private final MainActivity host;
public TitleClickListener (final MainActivity host) {
this.host = host;
}
@Override
public void onColumnTitleClick (final int argPosition) {
final int position = this.host.convertPagePosition(argPosition);
final Column col = this.host.getConf().getColumnByPosition(position);
if (col == null) return;
final TweetListFragment page = this.host.getActivePageById(col.getId());
if (page == null) return;
page.scrollJumpUp();
}
}
private static class NotificationClearingPageSelectionListener extends SimpleOnPageChangeListener {
private final MainActivity host;
public NotificationClearingPageSelectionListener (final MainActivity host) {
this.host = host;
}
@Override
public void onPageSelected (final int argPosition) {
final int position = this.host.convertPagePosition(argPosition);
final Column col = this.host.getConf().getColumnByPosition(position);
Notifications.clearColumn(this.host, col);
}
}
private static class CheckBackgroundUpdatesRunning extends TrackingAsyncTask<Void, Void, Boolean> {
private final Activity activity;
public CheckBackgroundUpdatesRunning (final Activity activity, final ExecutorEventListener eventListener) {
super(eventListener);
this.activity = activity;
}
@Override
protected Boolean doInBackgroundWithTracking (final Void... params) {
final float bl = BatteryHelper.level(this.activity.getApplicationContext());
return bl <= FetchingPrefFragment.readMinBatForUpdate(this.activity)
&& !BatteryNotify.isOverrideEnabled(this.activity);
}
@Override
protected void onPostExecute (final Boolean backgroudUpdatesDisabled) {
if (backgroudUpdatesDisabled) {
final View backgroundUpdateDetails = this.activity.findViewById(R.id.backgroudUpdateDetails);
backgroundUpdateDetails.setVisibility(View.VISIBLE);
this.activity.findViewById(R.id.backgroundUpdateOverride).setOnClickListener(new OnClickListener() {
@Override
public void onClick (final View v) {
BatteryNotify.plusTime(CheckBackgroundUpdatesRunning.this.activity);
backgroundUpdateDetails.setVisibility(View.GONE);
}
});
}
}
}
}