/*
* Copyright (c) 2015, Nils Braden
*
* This file is part of ttrss-reader-fork. 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/.
*/
package org.ttrssreader.gui;
import org.ttrssreader.R;
import org.ttrssreader.controllers.Controller;
import org.ttrssreader.controllers.DBHelper;
import org.ttrssreader.controllers.ProgressBarManager;
import org.ttrssreader.controllers.UpdateController;
import org.ttrssreader.gui.dialogs.ErrorDialog;
import org.ttrssreader.gui.dialogs.YesNoUpdaterDialog;
import org.ttrssreader.gui.interfaces.ICacheEndListener;
import org.ttrssreader.gui.interfaces.IDataChangedListener;
import org.ttrssreader.gui.interfaces.IItemSelectedListener;
import org.ttrssreader.gui.interfaces.IUpdateEndListener;
import org.ttrssreader.imageCache.ForegroundService;
import org.ttrssreader.model.updaters.IUpdatable;
import org.ttrssreader.model.updaters.StateSynchronisationUpdater;
import org.ttrssreader.model.updaters.Updater;
import org.ttrssreader.preferences.Constants;
import org.ttrssreader.utils.AsyncTask;
import org.ttrssreader.utils.PostMortemReportExceptionHandler;
import org.ttrssreader.utils.Utils;
import android.app.FragmentManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.net.ConnectivityManager;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
/**
* This class provides common functionality for Activities.
*/
public abstract class MenuActivity extends MenuFlavorActivity
implements IUpdateEndListener, ICacheEndListener, IItemSelectedListener, IDataChangedListener {
@SuppressWarnings("unused")
private static final String TAG = MenuActivity.class.getSimpleName();
private PostMortemReportExceptionHandler mDamageReport = new PostMortemReportExceptionHandler(this);
protected MenuActivity activity;
protected volatile boolean mOnSaveInstanceStateCalled = false;
private Updater updater;
private boolean isVertical;
private static int minSize;
private static int maxSize;
private int dividerSize;
private int displaySize;
private View frameMain = null;
private View divider = null;
private View frameSub = null;
private TextView header_title;
private TextView header_unread;
private ProgressBar progressbar;
private ProgressBar progressspinner;
@Override
protected void onCreate(Bundle instance) {
setTheme(Controller.getInstance().getTheme());
super.onCreate(instance);
mDamageReport.initialize();
activity = this;
Controller.getInstance().setHeadless(false);
setContentView(getLayoutResource());
initToolbar();
initTabletLayout();
}
protected abstract int getLayoutResource();
protected void initTabletLayout() {
frameMain = findViewById(R.id.frame_main);
divider = findViewById(R.id.list_divider);
frameSub = findViewById(R.id.frame_sub);
if (frameMain == null || frameSub == null || divider == null)
return; // Do nothing, the views do not exist...
// Initialize values for layout changes:
Controller
.refreshDisplayMetrics(((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay());
isVertical = (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT);
displaySize = Controller.displayWidth;
if (isVertical) {
TypedValue tv = new TypedValue();
getApplicationContext().getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true);
int actionBarHeight = getResources().getDimensionPixelSize(tv.resourceId);
displaySize = Controller.displayHeight - actionBarHeight;
}
minSize = (int) (displaySize * 0.05);
maxSize = displaySize - (int) (displaySize * 0.05);
// use tablet layout?
Controller.isTablet = (Controller.getInstance().allowTabletLayout() && divider != null);
// Set frame sizes and hide divider if necessary
if (Controller.isTablet) {
// Resize frames and do it only if stored size is within our bounds:
int mainFrameSize = Controller.getInstance().getMainFrameSize(this, isVertical, minSize, maxSize);
int subFrameSize = displaySize - mainFrameSize;
RelativeLayout.LayoutParams lpMain = (RelativeLayout.LayoutParams) frameMain.getLayoutParams();
RelativeLayout.LayoutParams lpSub = (RelativeLayout.LayoutParams) frameSub.getLayoutParams();
if (isVertical) {
// calculate height of divider
int padding = divider.getPaddingTop() + divider.getPaddingBottom();
dividerSize = Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, padding,
getApplicationContext().getResources().getDisplayMetrics()));
// Create LayoutParams for all three views
lpMain.height = mainFrameSize;
lpSub.height = subFrameSize - dividerSize;
} else {
// calculate width of divider
int padding = divider.getPaddingLeft() + divider.getPaddingRight();
dividerSize = Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, padding,
getApplicationContext().getResources().getDisplayMetrics()));
// Create LayoutParams for all three views
lpMain.width = mainFrameSize;
lpSub.width = subFrameSize - dividerSize;
}
// Set all params and visibility
frameMain.setLayoutParams(lpMain);
frameSub.setLayoutParams(lpSub);
divider.setVisibility(View.VISIBLE);
getWindow().getDecorView().getRootView().invalidate();
} else {
int match_parent = RelativeLayout.LayoutParams.MATCH_PARENT;
frameMain.setLayoutParams(new RelativeLayout.LayoutParams(match_parent, match_parent));
frameSub.setLayoutParams(new RelativeLayout.LayoutParams(match_parent, match_parent));
if (divider != null) divider.setVisibility(View.GONE);
}
}
private void handleResize() {
int mainFrameSize;
if (isVertical) {
mainFrameSize = calculateSize(frameMain.getHeight() + mDeltaY);
} else {
mainFrameSize = calculateSize(frameMain.getWidth() + mDeltaX);
}
int subFrameSize = displaySize - dividerSize - mainFrameSize;
RelativeLayout.LayoutParams lpMain = (RelativeLayout.LayoutParams) frameMain.getLayoutParams();
RelativeLayout.LayoutParams lpSub = (RelativeLayout.LayoutParams) frameSub.getLayoutParams();
if (isVertical) {
lpMain.height = mainFrameSize;
lpSub.height = subFrameSize;
} else {
lpMain.width = mainFrameSize;
lpSub.width = subFrameSize;
}
frameMain.setLayoutParams(lpMain);
frameSub.setLayoutParams(lpSub);
getWindow().getDecorView().getRootView().invalidate();
}
private void initToolbar() {
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
if (toolbar != null) {
setSupportActionBar(toolbar);
header_unread = (TextView) findViewById(R.id.head_unread);
header_title = (TextView) findViewById(R.id.head_title);
header_title.setText(getString(R.string.ApplicationName));
progressbar = (ProgressBar) findViewById(R.id.progressbar);
progressspinner = (ProgressBar) findViewById(R.id.progressspinner);
hideBackArrow();
}
}
protected void showBackArrow() {
ActionBar ab = getSupportActionBar();
if (ab != null) {
ab.setDisplayHomeAsUpEnabled(true);
ab.setDisplayShowTitleEnabled(false);
header_title.setPadding(0, 0, 0, 0);
}
}
protected void hideBackArrow() {
ActionBar ab = getSupportActionBar();
if (ab != null) {
ab.setDisplayHomeAsUpEnabled(false);
ab.setDisplayShowTitleEnabled(false);
header_title.setPadding(48, 0, 0, 0);
}
}
@Override
public void setTitle(CharSequence title) {
header_title.setText(title);
super.setTitle(title);
}
public void setUnread(int unread) {
header_unread.setVisibility(unread > 0 ? View.VISIBLE : View.GONE);
header_unread.setText(String.valueOf(unread));
}
@Override
protected void onResume() {
super.onResume();
if (Controller.getInstance().isScheduledRestart()) {
Controller.getInstance().setScheduledRestart(false);
Intent intent = getBaseContext().getPackageManager()
.getLaunchIntentForPackage(getBaseContext().getPackageName());
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
} else {
UpdateController.getInstance().registerActivity(this);
DBHelper.getInstance().initialize(this);
}
refreshAndUpdate();
}
@Override
protected void onStop() {
super.onStop();
UpdateController.getInstance().unregisterActivity(this);
}
@Override
protected void onDestroy() {
mDamageReport.restoreOriginalHandler();
mDamageReport = null;
super.onDestroy();
if (updater != null) {
updater.cancel(true);
updater = null;
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
mOnSaveInstanceStateCalled = true;
super.onSaveInstanceState(outState);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == ErrorActivity.ACTIVITY_SHOW_ERROR) {
refreshAndUpdate();
} else if (resultCode == Constants.ACTIVITY_SHOW_PREFERENCES) {
refreshAndUpdate();
} else if (resultCode == ErrorActivity.ACTIVITY_EXIT) {
finish();
} else if (resultCode == PreferencesActivity.ACTIVITY_RELOAD) {
finish();
startActivity(getIntent());
}
}
@Override
protected void onPostResume() {
super.onPostResume();
// Reset the boolean flag back to false for next time.
mOnSaveInstanceStateCalled = false;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.generic, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
MenuItem offline = menu.findItem(R.id.Menu_WorkOffline);
MenuItem refresh = menu.findItem(R.id.Menu_Refresh);
if (offline != null) {
if (Controller.getInstance().workOffline()) {
offline.setTitle(getString(R.string.UsageOnlineTitle));
offline.setIcon(R.drawable.ic_menu_play_clip);
if (refresh != null) menu.findItem(R.id.Menu_Refresh).setVisible(false);
} else {
offline.setTitle(getString(R.string.UsageOfflineTitle));
offline.setIcon(R.drawable.ic_menu_stop);
if (refresh != null) menu.findItem(R.id.Menu_Refresh).setVisible(true);
}
}
MenuItem displayUnread = menu.findItem(R.id.Menu_DisplayOnlyUnread);
if (displayUnread != null) {
if (Controller.getInstance().onlyUnread()) {
displayUnread.setTitle(getString(R.string.Commons_DisplayAll));
} else {
displayUnread.setTitle(getString(R.string.Commons_DisplayOnlyUnread));
}
}
MenuItem displayOnlyCachedImages = menu.findItem(R.id.Menu_DisplayOnlyCachedImages);
if (displayOnlyCachedImages != null) {
if (Controller.getInstance().onlyDisplayCachedImages()) {
displayOnlyCachedImages.setTitle(getString(R.string.Commons_DisplayAll));
} else {
displayOnlyCachedImages.setTitle(getString(R.string.Commons_DisplayOnlyCachedImages));
}
}
if (!(this instanceof FeedHeadlineActivity)) {
menu.removeItem(R.id.Menu_FeedUnsubscribe);
}
if (Controller.getInstance().hideFeedReadButtons()) {
menu.removeItem(R.id.Menu_MarkFeedRead);
menu.removeItem(R.id.Menu_MarkFeedsRead);
menu.removeItem(R.id.Menu_MarkAllRead);
}
MenuItem cache = menu.findItem(R.id.Category_Menu_ImageCache);
if (cache != null) {
if (isCacherRunning()) {
cache.setTitle(getString(R.string.Main_ImageCacheCancel));
} else {
cache.setTitle(getString(R.string.Main_ImageCache));
}
}
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
if (super.onOptionsItemSelected(item)) return true;
switch (item.getItemId()) {
case android.R.id.home:
// Go to the CategoryActivity and clean the return-stack
Intent intent = new Intent(this, CategoryActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
return true;
case R.id.Menu_DisplayOnlyUnread:
Controller.getInstance().setDisplayOnlyUnread(!Controller.getInstance().onlyUnread());
doRefresh();
return true;
case R.id.Menu_DisplayOnlyCachedImages:
Controller.getInstance().setDisplayCachedImages(!Controller.getInstance().onlyDisplayCachedImages());
doRefresh();
return true;
case R.id.Menu_InvertSort:
if (this instanceof FeedHeadlineActivity) {
Controller.getInstance()
.setInvertSortArticleList(!Controller.getInstance().invertSortArticlelist());
} else {
Controller.getInstance().setInvertSortFeedsCats(!Controller.getInstance().invertSortFeedscats());
}
doRefresh();
return true;
case R.id.Menu_WorkOffline:
Controller.getInstance().setWorkOffline(!Controller.getInstance().workOffline());
if (!Controller.getInstance().workOffline()) {
// Synchronize status of articles with server
new Updater(this, new StateSynchronisationUpdater())
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
doRefresh();
return true;
case R.id.Menu_ShowPreferences:
startActivityForResult(new Intent(this, PreferencesActivity.class),
Constants.ACTIVITY_SHOW_PREFERENCES);
return true;
case R.id.Menu_About:
startActivity(new Intent(this, AboutActivity.class));
return true;
case R.id.Category_Menu_ImageCache:
if (isCacherRunning()) doStopImageCache();
else doStartImageCache();
return true;
case R.id.Menu_FeedSubscribe:
startActivity(new Intent(this, SubscribeActivity.class));
return true;
default:
return false;
}
}
@Override
public void onUpdateEnd(boolean goBackAfterUpdate) {
updater = null;
doRefresh();
if (goBackAfterUpdate && !isFinishing()) onBackPressed();
}
protected void doStopImageCache() {
ForegroundService.cancel();
invalidateOptionsMenu();
}
protected void doStartImageCache() {
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
final int title = R.string.Main_ImageCache_YesNoTitle;
final FragmentManager fm = getFragmentManager();
switch (Utils.getNetworkType(cm)) {
case Utils.NETWORK_MOBILE:
YesNoUpdaterDialog.getInstance(new ImageCacheUpdater(Utils.NETWORK_MOBILE), title,
R.string.Main_ImageCache_NetworkMobile).show(fm, "imagecache");
break;
case Utils.NETWORK_METERED:
YesNoUpdaterDialog.getInstance(new ImageCacheUpdater(Utils.NETWORK_METERED), title,
R.string.Main_ImageCache_NetworkMetered).show(fm, "imagecache");
break;
case Utils.NETWORK_WIFI:
doCache(Utils.NETWORK_WIFI);
break;
}
}
private class ImageCacheUpdater implements IUpdatable {
int networkState;
public ImageCacheUpdater(int networkState) {
this.networkState = networkState;
}
@Override
public void update() {
doCache(networkState);
}
}
/* ############# BEGIN: Cache */
private void doCache(final int networkState) {
// Register for progress-updates
ForegroundService.registerCallback(this);
if (isCacherRunning()) return;
invalidateOptionsMenu();
// Start new cacher
Intent intent = new Intent(ForegroundService.ACTION_LOAD_IMAGES);
intent.putExtra(ForegroundService.PARAM_NETWORK, networkState);
intent.setClass(this.getApplicationContext(), ForegroundService.class);
this.startService(intent);
ProgressBarManager.getInstance().addProgress(this);
}
@Override
public void onCacheEnd() {
ProgressBarManager.getInstance().removeProgress(this);
}
@Override
public void onCacheInterrupted() {
ProgressBarManager.getInstance().removeProgress(this);
Toast.makeText(this, R.string.Main_ImageCache_ConnectivityLost, Toast.LENGTH_SHORT).show();
}
@Override
public void onCacheProgress(int taskCount, int progress) {
if (taskCount == 0) setSupportProgress(0);
else setSupportProgress((10000 / taskCount) * progress);
}
protected boolean isCacherRunning() {
return ForegroundService.isInstanceCreated();
}
/* ############# END: Cache */
protected void openConnectionErrorDialog(String errorMessage) {
if (updater != null) {
updater.cancel(true);
updater = null;
}
ProgressBarManager.getInstance().resetProgress(this);
Intent i = new Intent(this, ErrorActivity.class);
i.putExtra(ErrorActivity.ERROR_MESSAGE, errorMessage);
startActivityForResult(i, ErrorActivity.ACTIVITY_SHOW_ERROR);
}
protected void showErrorDialog(String message) {
ErrorDialog.getInstance(message).show(getFragmentManager(), "error");
}
private void refreshAndUpdate() {
initTabletLayout();
if (!Utils.checkIsConfigInvalid()) {
doUpdate(false);
doRefresh();
}
}
@Override
public final void dataChanged() {
doRefresh();
}
@Override
public void dataLoadingFinished() {
// Empty!
}
protected void doRefresh() {
invalidateOptionsMenu();
ProgressBarManager.getInstance().setIndeterminateVisibility(this);
if (Controller.getInstance().getConnector().hasLastError())
openConnectionErrorDialog(Controller.getInstance().getConnector().pullLastError());
}
protected abstract void doUpdate(boolean forceUpdate);
/**
* Can be used in child activities to update their data and get a UI refresh afterwards.
*/
abstract class ActivityUpdater extends AsyncTask<Void, Integer, Void> {
protected int taskCount = 0;
protected boolean forceUpdate;
ActivityUpdater(boolean forceUpdate) {
this.forceUpdate = forceUpdate;
ProgressBarManager.getInstance().addProgress(activity);
}
@Override
protected void onProgressUpdate(Integer... values) {
if (values[0] == Integer.MAX_VALUE) {
if (!isCacherRunning()) ProgressBarManager.getInstance().removeProgress(activity);
return;
}
// Add 500 to make sure we are still within 10000 but never show an empty progressbar at 0
setSupportProgress((10000 / (taskCount + 1)) * values[0] + 500);
}
}
// The "active pointer" is the one currently moving our object.
private static final int INVALID_POINTER_ID = -1;
private int mActivePointerId = INVALID_POINTER_ID;
private float mLastTouchX = 0;
private float mLastTouchY = 0;
private int mDeltaX = 0;
private int mDeltaY = 0;
private boolean resizing = false;
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!Controller.isTablet) return false;
// Only handle events when the list-divider is selected or we are already resizing:
View view = findViewAtPosition(getWindow().getDecorView().getRootView(), (int) ev.getRawX(),
(int) ev.getRawY());
if (view == null && !resizing) return false;
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
divider.setSelected(true);
resizing = true;
final int pointerIndex = ev.getActionIndex();
// Remember where we started (for dragging)
mLastTouchX = ev.getX(pointerIndex);
mLastTouchY = ev.getY(pointerIndex);
mDeltaX = 0;
mDeltaY = 0;
// Save the ID of this pointer (for dragging)
mActivePointerId = ev.getPointerId(0);
break;
}
case MotionEvent.ACTION_MOVE: {
// Find the index of the active pointer and fetch its position
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) break;
final float x = ev.getX(pointerIndex);
final float y = ev.getY(pointerIndex);
// Calculate the distance moved
mDeltaX = (int) (x - mLastTouchX);
mDeltaY = (int) (y - mLastTouchY);
// Store location for next difference
mLastTouchX = x;
mLastTouchY = y;
handleResize();
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
default:
mActivePointerId = INVALID_POINTER_ID;
handleResize();
storeSize();
divider.setSelected(false);
resizing = false;
break;
}
return true;
}
private int calculateSize(final int size) {
int ret = size;
if (ret < minSize) ret = minSize;
if (ret > maxSize) ret = maxSize;
return ret;
}
private void storeSize() {
int size = isVertical ? frameMain.getHeight() : frameMain.getWidth();
Controller.getInstance().setViewSize(this, isVertical, size);
}
private static View findViewAtPosition(View parent, int x, int y) {
if (parent instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) parent;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View child = viewGroup.getChildAt(i);
View viewAtPosition = findViewAtPosition(child, x, y);
if (viewAtPosition != null) {
return viewAtPosition;
}
}
return null;
} else {
Rect rect = new Rect();
parent.getGlobalVisibleRect(rect);
if (rect.contains(x, y)) {
return parent;
} else {
return null;
}
}
}
@Override
public void setSupportProgress(int progress) {
setSupportProgressBarVisibility(progress > 1 && progress < 9999);
progressbar.setProgress(progress);
super.setSupportProgress(progress);
}
@Override
public void setSupportProgressBarVisibility(boolean visible) {
progressbar.setVisibility(visible ? View.VISIBLE : View.GONE);
super.setSupportProgressBarVisibility(visible);
}
@Override
public void setSupportProgressBarIndeterminateVisibility(boolean visible) {
progressspinner.setVisibility(visible ? View.VISIBLE : View.GONE);
super.setSupportProgressBarIndeterminateVisibility(visible);
}
}