org.andstatus.app.msg.TimelineActivity.java Source code

Java tutorial

Introduction

Here is the source code for org.andstatus.app.msg.TimelineActivity.java

Source

/* 
 * Copyright (c) 2011-2015 yvolk (Yuri Volkov), http://yurivolkov.com
 *
 * 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 org.andstatus.app.msg;

import android.app.AlertDialog;
import android.app.Dialog;
import android.app.LoaderManager.LoaderCallbacks;
import android.app.SearchManager;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.Loader;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.SearchRecentSuggestions;
import android.support.v4.widget.DrawerLayout;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.CheckBox;
import android.widget.CursorAdapter;
import android.widget.LinearLayout;
import android.widget.TextView;

import org.andstatus.app.ActivityRequestCode;
import org.andstatus.app.HelpActivity;
import org.andstatus.app.IntentExtra;
import org.andstatus.app.MyAction;
import org.andstatus.app.MyBaseListActivity;
import org.andstatus.app.MyListActivity;
import org.andstatus.app.R;
import org.andstatus.app.account.AccountSelector;
import org.andstatus.app.account.MyAccount;
import org.andstatus.app.context.MyContextHolder;
import org.andstatus.app.context.MySettingsActivity;
import org.andstatus.app.context.MyPreferences;
import org.andstatus.app.data.MatchedUri;
import org.andstatus.app.data.MyDatabase;
import org.andstatus.app.data.MyDatabase.User;
import org.andstatus.app.data.MyQuery;
import org.andstatus.app.data.TimelineSearchSuggestionsProvider;
import org.andstatus.app.data.TimelineType;
import org.andstatus.app.service.CommandData;
import org.andstatus.app.service.CommandEnum;
import org.andstatus.app.service.MyServiceEvent;
import org.andstatus.app.service.MyServiceEventsListener;
import org.andstatus.app.service.MyServiceManager;
import org.andstatus.app.service.MyServiceEventsReceiver;
import org.andstatus.app.service.QueueViewer;
import org.andstatus.app.test.SelectorActivityMock;
import org.andstatus.app.util.I18n;
import org.andstatus.app.util.InstanceId;
import org.andstatus.app.util.MyLog;
import org.andstatus.app.util.UriUtils;
import org.andstatus.app.widget.MySimpleCursorAdapter;
import org.andstatus.app.widget.MySwipeRefreshLayout;

import java.util.List;

/**
 * @author yvolk@yurivolkov.com
 */
public class TimelineActivity extends MyListActivity
        implements MyServiceEventsListener, ActionableMessageList, LoaderCallbacks<Cursor> {
    private static final int DIALOG_ID_TIMELINE_TYPE = 9;
    private static final int LOADER_ID = 1;
    private static final String ACTIVITY_PERSISTENCE_NAME = TimelineActivity.class.getSimpleName();

    private MySwipeRefreshLayout mSwipeRefreshLayout = null;

    /** Parameters of currently shown Timeline */
    private TimelineListParameters mListParametersLoaded;
    private TimelineListParameters mListParametersNew;

    /**
     * The is no more items in the query, so don't try to load more pages
     */
    private boolean mNoMoreItems = false;

    /**
     * For testing purposes
     */
    private long mInstanceId = 0;
    MyServiceEventsReceiver mServiceConnector;

    /**
     * We are going to finish/restart this Activity (e.g. onResume or even onCreate)
     */
    private volatile boolean mFinishing = false;

    private boolean mShowSyncIndicatorOnTimeline = false;
    private View mSyncIndicator = null;

    /**
     * Time when shared preferences where changed
     */
    private long mPreferencesChangeTime = 0;

    private MessageContextMenu mContextMenu;
    private MessageEditor mMessageEditor;

    private String mTextToShareViaThisApp = "";
    private Uri mMediaToShareViaThisApp = Uri.EMPTY;

    private String mRateLimitText = "";

    DrawerLayout mDrawerLayout;
    ActionBarDrawerToggle mDrawerToggle;
    protected volatile SelectorActivityMock selectorActivityMock;

    /**
     * This method is the first of the whole application to be called 
     * when the application starts for the very first time.
     * So we may put some Application initialization code here. 
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        mListParametersLoaded = new TimelineListParameters(this);
        mListParametersNew = new TimelineListParameters(this);
        if (mInstanceId == 0) {
            mInstanceId = InstanceId.next();
        } else {
            MyLog.d(this, "onCreate reusing the same instanceId=" + mInstanceId);
        }

        mPreferencesChangeTime = MyContextHolder.initialize(this, this);
        mShowSyncIndicatorOnTimeline = MyPreferences.getBoolean(MyPreferences.KEY_SYNC_INDICATOR_ON_TIMELINE, true);

        if (MyLog.isLoggable(this, MyLog.DEBUG)) {
            MyLog.d(this, "onCreate instanceId=" + mInstanceId + " , preferencesChangeTime="
                    + mPreferencesChangeTime + (MyContextHolder.get().isReady() ? "" : ", MyContext is not ready"));
        }
        mLayoutId = R.layout.timeline;
        super.onCreate(savedInstanceState);

        if (HelpActivity.startFromActivity(this)) {
            return;
        }

        mListParametersNew.myAccountUserId = MyContextHolder.get().persistentAccounts().getCurrentAccountUserId();
        mServiceConnector = new MyServiceEventsReceiver(this);

        mSyncIndicator = findViewById(R.id.sync_indicator);
        mContextMenu = new MessageContextMenu(this);
        mMessageEditor = new MessageEditor(this);
        initializeSwipeRefresh();

        restoreActivityState();

        initializeDrawer();

        if (savedInstanceState == null) {
            parseNewIntent(getIntent());
        }
        getLoaderManager().initLoader(LOADER_ID, null, this);

        updateScreen();
        queryListData(false);
    }

    private void initializeSwipeRefresh() {
        mSwipeRefreshLayout = (MySwipeRefreshLayout) findViewById(R.id.myLayoutParent);
        mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                manualReload(false, true);
            }
        });
    }

    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        mDrawerToggle.syncState();
    }

    private void initializeDrawer() {
        mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
        mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, R.string.drawer_open,
                R.string.drawer_close) {
        };

        mDrawerLayout.setDrawerListener(mDrawerToggle);

        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setHomeButtonEnabled(true);
    }

    private void restoreActivityState() {
        SharedPreferences activityState = MyPreferences.getSharedPreferences(ACTIVITY_PERSISTENCE_NAME);
        if (activityState != null) {
            if (mListParametersNew.restoreState(activityState)) {
                mContextMenu.loadState(activityState);
            }
        }
    }

    /**
     * View.OnClickListener
     */
    public void onCombinedTimelineToggleClick(View item) {
        closeDrawer();
        boolean on = !isTimelineCombined();
        MyPreferences.getDefaultSharedPreferences().edit().putBoolean(MyPreferences.KEY_TIMELINE_IS_COMBINED, on)
                .commit();
        mContextMenu.switchTimelineActivity(mListParametersNew.getTimelineType(), on,
                mListParametersNew.myAccountUserId);
    }

    private void closeDrawer() {
        ViewGroup mDrawerList = (ViewGroup) findViewById(R.id.navigation_drawer);
        mDrawerLayout.closeDrawer(mDrawerList);
    }

    /**
     * View.OnClickListener
     */
    public void onTimelineTypeButtonClick(View item) {
        showDialog(DIALOG_ID_TIMELINE_TYPE);
        closeDrawer();
    }

    /**
     * View.OnClickListener
     */
    public void onSelectAccountButtonClick(View item) {
        if (MyContextHolder.get().persistentAccounts().size() > 1) {
            AccountSelector.selectAccount(TimelineActivity.this, 0, ActivityRequestCode.SELECT_ACCOUNT);
        }
        closeDrawer();
    }

    /**
     * See <a href="http://developer.android.com/guide/topics/search/search-dialog.html">Creating 
     * a Search Interface</a>
     */
    @Override
    public boolean onSearchRequested() {
        return onSearchRequested(false);
    }

    private boolean onSearchRequested(boolean appGlobalSearch) {
        final String method = "onSearchRequested";
        Bundle appSearchData = new Bundle();
        appSearchData.putString(IntentExtra.TIMELINE_URI.key,
                mListParametersNew.getTimelineUri(appGlobalSearch).toString());
        appSearchData.putBoolean(IntentExtra.GLOBAL_SEARCH.key, appGlobalSearch);
        MyLog.v(this, method + ": " + appSearchData);
        startSearch(null, false, appSearchData, false);
        return true;
    }

    @Override
    protected void onResume() {
        String method = "onResume";
        super.onResume();
        MyLog.v(this, method + ", instanceId=" + mInstanceId);
        if (!mFinishing) {
            if (MyContextHolder.get().persistentAccounts().getCurrentAccount().isValid()) {
                long preferencesChangeTimeNew = MyContextHolder.initialize(this, this);
                if (preferencesChangeTimeNew != mPreferencesChangeTime) {
                    MyLog.v(this, method + "; Restarting this Activity to apply all new changes of preferences");
                    finish();
                    mContextMenu.switchTimelineActivity(mListParametersNew.getTimelineType(),
                            mListParametersNew.isTimelineCombined(), mListParametersNew.mSelectedUserId);
                }
            } else {
                MyLog.v(this, method + "; Finishing this Activity because there is no Account selected");
                finish();
            }
        }
        if (!mFinishing) {
            MyContextHolder.get().setInForeground(true);
            mServiceConnector.registerReceiver(this);
            mMessageEditor.loadCurrentDraft();
        }
    }

    @Override
    public void onContentChanged() {
        if (MyLog.isLoggable(this, MyLog.DEBUG)) {
            MyLog.d(this, "onContentChanged started");
        }
        super.onContentChanged();
    }

    @Override
    protected void onPause() {
        final String method = "onPause";
        if (MyLog.isVerboseEnabled()) {
            MyLog.v(this, method + "; instanceId=" + mInstanceId);
        }
        mServiceConnector.unregisterReceiver(this);
        setSyncIndicator(method, false);
        mMessageEditor.saveAsBeingEditedAndHide();
        saveActivityState();
        super.onPause();
        MyContextHolder.get().setInForeground(false);
    }

    private void setSyncIndicator(String source, boolean isVisible) {
        if (isVisible ? (mSyncIndicator.getVisibility() != View.VISIBLE)
                : ((mSyncIndicator.getVisibility() == View.VISIBLE))) {
            MyLog.v(this, source + " set Sync indicator to " + isVisible);
            mSyncIndicator.setVisibility(isVisible ? View.VISIBLE : View.GONE);
        }
    }

    /**
     * Cancel notifications of loading timeline, which were set during Timeline downloading
     */
    private void clearNotifications() {
        MyContextHolder.get().clearNotification(getTimelineType());
        MyServiceManager.sendForegroundCommand(new CommandData(CommandEnum.NOTIFY_CLEAR, MyContextHolder.get()
                .persistentAccounts().fromUserId(mListParametersNew.myAccountUserId).getAccountName()));
    }

    @Override
    public void onDestroy() {
        MyLog.v(this, "onDestroy, instanceId=" + mInstanceId);
        if (mServiceConnector != null) {
            mServiceConnector.unregisterReceiver(this);
        }
        super.onDestroy();
    }

    @Override
    public void finish() {
        MyLog.v(this,
                "Finish requested" + (mFinishing ? ", already finishing" : "") + ", instanceId=" + mInstanceId);
        if (!mFinishing) {
            mFinishing = true;
        }
        if (getList() != null) {
            getList().savePositionOnUiThread();
        }
        super.finish();
    }

    @Override
    protected Dialog onCreateDialog(int id) {
        switch (id) {
        case DIALOG_ID_TIMELINE_TYPE:
            return newTimelineTypeSelector();
        default:
            break;
        }
        return super.onCreateDialog(id);
    }

    // TODO: Replace this with http://developer.android.com/reference/android/app/DialogFragment.html
    private AlertDialog newTimelineTypeSelector() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle(R.string.dialog_title_select_timeline);
        final TimelineTypeSelector selector = new TimelineTypeSelector(this);
        builder.setItems(selector.getTitles(), new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // The 'which' argument contains the index position of the
                // selected item
                TimelineType type = selector.positionToType(which);
                if (type != TimelineType.UNKNOWN) {
                    mContextMenu.switchTimelineActivity(type, mListParametersNew.isTimelineCombined(),
                            mListParametersNew.myAccountUserId);
                }
            }
        });
        return builder.create();
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        mContextMenu.onContextItemSelected(item);
        return super.onContextItemSelected(item);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.timeline, menu);
        if (mMessageEditor != null) {
            mMessageEditor.onCreateOptionsMenu(menu);
        }
        return true;
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        MyAccount ma = MyContextHolder.get().persistentAccounts().getCurrentAccount();
        boolean enableReload = isTimelineCombined() || ma.isValidAndSucceeded();
        MenuItem item = menu.findItem(R.id.reload_menu_item);
        item.setEnabled(enableReload);
        item.setVisible(enableReload);

        prepareDrawer();

        if (mContextMenu != null) {
            mContextMenu.setAccountUserIdToActAs(0);
        }

        if (mMessageEditor != null) {
            mMessageEditor.onPrepareOptionsMenu(menu);
        }

        boolean enableGlobalSearch = MyContextHolder.get().persistentAccounts().isGlobalSearchSupported(ma,
                isTimelineCombined());
        item = menu.findItem(R.id.global_search_menu_id);
        item.setEnabled(enableGlobalSearch);
        item.setVisible(enableGlobalSearch);

        return super.onPrepareOptionsMenu(menu);
    }

    private void prepareDrawer() {
        ViewGroup mDrawerList = (ViewGroup) findViewById(R.id.navigation_drawer);
        if (mDrawerList == null) {
            return;
        }
        TextView item = (TextView) mDrawerList.findViewById(R.id.timelineTypeButton);
        item.setText(timelineTypeButtonText());
        prepareCombinedTimelineToggle(mDrawerList);
        updateAccountButtonText(mDrawerList);
    }

    private void prepareCombinedTimelineToggle(ViewGroup list) {
        CheckBox combinedTimelineToggle = (CheckBox) list.findViewById(R.id.combinedTimelineToggle);
        combinedTimelineToggle.setChecked(isTimelineCombined());
        if (mListParametersNew.mSelectedUserId != 0
                && mListParametersNew.mSelectedUserId != mListParametersNew.myAccountUserId) {
            combinedTimelineToggle.setVisibility(View.GONE);
        } else {
            // Show the "Combined" toggle even for one account to see messages, 
            // which are not on the timeline.
            // E.g. messages by users, downloaded on demand.
            combinedTimelineToggle.setVisibility(View.VISIBLE);
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (mDrawerToggle.onOptionsItemSelected(item)) {
            return true;
        }
        switch (item.getItemId()) {
        case R.id.global_search_menu_id:
            onSearchRequested(true);
            break;
        case R.id.search_menu_id:
            onSearchRequested();
            break;
        case R.id.reload_menu_item:
            manualReload(false, true);
            break;
        case R.id.commands_queue_id:
            startActivity(new Intent(getActivity(), QueueViewer.class));
            break;
        case R.id.preferences_menu_id:
            startMyPreferenceActivity();
            break;
        case R.id.help_menu_id:
            onHelp();
            break;
        default:
            break;
        }
        return super.onOptionsItemSelected(item);
    }

    private void onHelp() {
        Intent intent = new Intent(this, HelpActivity.class);
        intent.putExtra(HelpActivity.EXTRA_HELP_PAGE_INDEX, HelpActivity.PAGE_INDEX_USER_GUIDE);
        startActivity(intent);
    }

    public void onItemClick(AdapterView<?> adapterView, final View view, final int position, final long id) {
        if (id <= 0) {
            if (MyLog.isVerboseEnabled()) {
                MyLog.v(this, "onItemClick, position=" + position + "; id=" + id + "; view=" + view);
            }
            return;
        }

        new AsyncTask<Void, Void, Uri>() {

            @Override
            protected Uri doInBackground(Void... params) {
                long linkedUserId = getLinkedUserIdFromCursor(position);
                MyAccount ma = MyContextHolder.get().persistentAccounts().getAccountForThisMessage(id, linkedUserId,
                        mListParametersNew.myAccountUserId, false);
                if (MyLog.isVerboseEnabled()) {
                    MyLog.v(this, "onItemClick, position=" + position + "; id=" + id + "; view=" + view
                            + "; linkedUserId=" + linkedUserId + " account=" + ma.getAccountName());
                }
                return MatchedUri.getTimelineItemUri(ma.getUserId(), mListParametersNew.getTimelineType(),
                        mListParametersNew.isTimelineCombined(), mListParametersNew.getSelectedUserId(), id);
            }

            @Override
            protected void onPostExecute(Uri uri) {
                String action = getIntent().getAction();
                if (Intent.ACTION_PICK.equals(action) || Intent.ACTION_GET_CONTENT.equals(action)) {
                    if (MyLog.isLoggable(this, MyLog.DEBUG)) {
                        MyLog.d(this, "onItemClick, setData=" + uri);
                    }
                    setResult(RESULT_OK, new Intent().setData(uri));
                } else {
                    if (MyLog.isLoggable(this, MyLog.DEBUG)) {
                        MyLog.d(this, "onItemClick, startActivity=" + uri);
                    }
                    startActivity(MyAction.VIEW_CONVERSATION.getIntent(uri));
                }
            }

        }.execute();
    }

    /**
     * @param position Of current item in the underlying Cursor
     * @return id of the User linked to this message. This link reflects the User's timeline 
     * or an Account which was used to retrieved the message
     */
    @Override
    public long getLinkedUserIdFromCursor(int position) {
        long userId = 0;
        try {
            Cursor cursor = null;
            if (getListAdapter() != null) {
                cursor = ((CursorAdapter) getListAdapter()).getCursor();
            }
            if (cursor != null && !cursor.isClosed()) {
                cursor.moveToPosition(position);
                int columnIndex = cursor.getColumnIndex(User.LINKED_USER_ID);
                if (columnIndex > -1) {
                    userId = cursor.getLong(columnIndex);
                }
            }
        } catch (Exception e) {
            MyLog.v(this, e);
        }
        return userId;
    }

    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        if (!mNoMoreItems) {
            // Idea from http://stackoverflow.com/questions/1080811/android-endless-list
            boolean loadMore = (visibleItemCount > 0) && (firstVisibleItem > 0)
                    && (firstVisibleItem + visibleItemCount >= totalItemCount);
            if (loadMore) {
                MyLog.d(this, "Start Loading more items, rows=" + totalItemCount);
                queryListData(true);
            }
        }
    }

    private String timelineTypeButtonText() {
        CharSequence timelineName = mListParametersNew.getTimelineType().getTitle(this);
        return timelineName + (TextUtils.isEmpty(mListParametersNew.mSearchQuery) ? "" : " *");
    }

    private void updateAccountButtonText(ViewGroup mDrawerList) {
        TextView selectAccountButton = (TextView) mDrawerList.findViewById(R.id.selectAccountButton);
        String accountButtonText = buildAccountButtonText(mListParametersNew.myAccountUserId);
        selectAccountButton.setText(accountButtonText);
    }

    public static String buildAccountButtonText(long myAccountUserId) {
        MyAccount ma = MyContextHolder.get().persistentAccounts().fromUserId(myAccountUserId);
        String accountButtonText = ma.shortestUniqueAccountName();
        if (!ma.isValidAndSucceeded()) {
            accountButtonText = "(" + accountButtonText + ")";
        }
        return accountButtonText;
    }

    @Override
    protected void onNewIntent(Intent intent) {
        if (MyLog.isVerboseEnabled()) {
            MyLog.v(this, "onNewIntent, instanceId=" + mInstanceId + (mFinishing ? ", Is finishing" : ""));
        }
        if (mFinishing) {
            finish();
            return;
        }
        super.onNewIntent(intent);
        MyContextHolder.initialize(this, this);
        parseNewIntent(intent);
        updateScreen();
        queryListData(false);
    }

    private void parseNewIntent(Intent intentNew) {
        if (MyLog.isVerboseEnabled()) {
            MyLog.v(this, "parseNewIntent:" + intentNew);
        }
        mRateLimitText = "";
        mListParametersNew.setTimelineType(TimelineType.UNKNOWN);
        mListParametersNew.myAccountUserId = MyContextHolder.get().persistentAccounts().getCurrentAccountUserId();
        mListParametersNew.mSelectedUserId = 0;
        parseAppSearchData(intentNew);
        if (mListParametersNew.getTimelineType() == TimelineType.UNKNOWN) {
            mListParametersNew.parseIntentData(intentNew);
        }
        if (mListParametersNew.getTimelineType() == TimelineType.UNKNOWN) {
            /* Set default values */
            mListParametersNew.setTimelineType(TimelineType.HOME);
            mListParametersNew.mSearchQuery = "";
        }
        if (mListParametersNew.getTimelineType() == TimelineType.USER) {
            if (mListParametersNew.mSelectedUserId == 0) {
                mListParametersNew.mSelectedUserId = mListParametersNew.myAccountUserId;
            }
        } else {
            mListParametersNew.mSelectedUserId = 0;
        }

        if (Intent.ACTION_SEND.equals(intentNew.getAction())) {
            shareViaThisApplication(intentNew.getStringExtra(Intent.EXTRA_SUBJECT),
                    intentNew.getStringExtra(Intent.EXTRA_TEXT),
                    (Uri) intentNew.getParcelableExtra(Intent.EXTRA_STREAM));
        }
    }

    private void parseAppSearchData(Intent intentNew) {
        Bundle appSearchData = intentNew.getBundleExtra(SearchManager.APP_DATA);
        if (appSearchData == null || !mListParametersNew
                .parseUri(Uri.parse(appSearchData.getString(IntentExtra.TIMELINE_URI.key, "")))) {
            return;
        }
        /* The query itself is still from the Intent */
        mListParametersNew.mSearchQuery = TimelineListParameters
                .notNullString(intentNew.getStringExtra(SearchManager.QUERY));
        if (!TextUtils.isEmpty(mListParametersNew.mSearchQuery)
                && appSearchData.getBoolean(IntentExtra.GLOBAL_SEARCH.key, false)) {
            setSyncing("Global search: " + mListParametersNew.mSearchQuery, true);
            MyServiceManager
                    .sendManualForegroundCommand(
                            CommandData
                                    .searchCommand(
                                            isTimelineCombined() ? ""
                                                    : MyContextHolder.get().persistentAccounts()
                                                            .getCurrentAccountName(),
                                            mListParametersNew.mSearchQuery));
        }
    }

    private void shareViaThisApplication(String subject, String text, Uri mediaUri) {
        if (TextUtils.isEmpty(subject) && TextUtils.isEmpty(text) && UriUtils.isEmpty(mediaUri)) {
            return;
        }
        mTextToShareViaThisApp = "";
        mMediaToShareViaThisApp = mediaUri;
        if (subjectHasAdditionalContent(subject, text)) {
            mTextToShareViaThisApp += subject;
        }
        if (!TextUtils.isEmpty(text)) {
            if (!TextUtils.isEmpty(mTextToShareViaThisApp)) {
                mTextToShareViaThisApp += " ";
            }
            mTextToShareViaThisApp += text;
        }
        MyLog.v(this, "Share via this app "
                + (!TextUtils.isEmpty(mTextToShareViaThisApp) ? "; text:'" + mTextToShareViaThisApp + "'" : "")
                + (!UriUtils.isEmpty(mMediaToShareViaThisApp) ? "; media:" + mMediaToShareViaThisApp.toString()
                        : ""));
        AccountSelector.selectAccount(this, 0, ActivityRequestCode.SELECT_ACCOUNT_TO_SHARE_VIA);
    }

    static boolean subjectHasAdditionalContent(String subject, String text) {
        if (TextUtils.isEmpty(subject)) {
            return false;
        }
        if (TextUtils.isEmpty(text)) {
            return true;
        }
        return !text.startsWith(stripEllipsis(stripBeginning(subject)));
    }

    /**
     * Strips e.g. "Message - " or "Message:"
     */
    static String stripBeginning(String textIn) {
        if (TextUtils.isEmpty(textIn)) {
            return "";
        }
        int ind = textIn.indexOf("-");
        if (ind < 0) {
            ind = textIn.indexOf(":");
        }
        if (ind < 0) {
            return textIn;
        }
        String beginningSeparators = "-:;,.[] ";
        while ((ind < textIn.length()) && beginningSeparators.contains(String.valueOf(textIn.charAt(ind)))) {
            ind++;
        }
        if (ind >= textIn.length()) {
            return textIn;
        }
        return textIn.substring(ind);
    }

    static String stripEllipsis(String textIn) {
        if (TextUtils.isEmpty(textIn)) {
            return "";
        }
        int ind = textIn.length() - 1;
        String ellipsis = " .";
        while (ind >= 0 && ellipsis.contains(String.valueOf(textIn.charAt(ind)))) {
            ind--;
        }
        if (ind < -1) {
            return "";
        }
        return textIn.substring(0, ind + 1);
    }

    private void updateScreen() {
        MyServiceManager.setServiceAvailable();
        invalidateOptionsMenu();
        mMessageEditor.updateScreen();
        updateTitle();
    }

    private void updateTitle() {
        new TimelineTitle(mListParametersLoaded.getTimelineType() != TimelineType.UNKNOWN ? mListParametersLoaded
                : mListParametersNew, mRateLimitText).updateTitle(this);
    }

    public MessageContextMenu getContextMenu() {
        return mContextMenu;
    }

    public void saveListPosition() {
        TimelineFragment list = getList();
        if (list != null) {
            new TimelineListPositionStorage(getListAdapter(), list.getListView(), mListParametersLoaded).save();
        }
    }

    static class TimelineTitle {
        final StringBuilder title = new StringBuilder();
        final StringBuilder subTitle = new StringBuilder();

        public TimelineTitle(TimelineListParameters ta, String additionalTitleText) {
            buildTitle(ta);
            buildSubtitle(ta, additionalTitleText);
        }

        private void buildTitle(TimelineListParameters ta) {
            I18n.appendWithSpace(title, ta.getTimelineType().getTitle(ta.mContext));
            if (!TextUtils.isEmpty(ta.mSearchQuery)) {
                I18n.appendWithSpace(title, "'" + ta.mSearchQuery + "'");
            }
            if (ta.getTimelineType() == TimelineType.USER && !(ta.isTimelineCombined()
                    && MyContextHolder.get().persistentAccounts().fromUserId(ta.getSelectedUserId()).isValid())) {
                I18n.appendWithSpace(title, MyQuery.userIdToWebfingerId(ta.getSelectedUserId()));
            }
            if (ta.isTimelineCombined()) {
                I18n.appendWithSpace(title, ta.mContext.getText(R.string.combined_timeline_on));
            }
        }

        private void buildSubtitle(TimelineListParameters ta, String additionalTitleText) {
            if (!ta.isTimelineCombined()) {
                I18n.appendWithSpace(subTitle,
                        ta.getTimelineType().getPrepositionForNotCombinedTimeline(ta.mContext));
                if (ta.getTimelineType().atOrigin()) {
                    I18n.appendWithSpace(subTitle, MyContextHolder.get().persistentAccounts()
                            .fromUserId(ta.getMyAccountUserId()).getOrigin().getName() + ";");
                }
            }
            I18n.appendWithSpace(subTitle, buildAccountButtonText(ta.getMyAccountUserId()));
            I18n.appendWithSpace(subTitle, additionalTitleText);
        }

        private void updateTitle(AppCompatActivity activity) {
            ActionBar actionBar = activity.getSupportActionBar();
            if (actionBar != null) {
                actionBar.setTitle(title);
                actionBar.setSubtitle(subTitle);
            }
            if (MyLog.isVerboseEnabled()) {
                MyLog.v(activity, "Title: " + toString());
            }
        }

        @Override
        public String toString() {
            return title + "; " + subTitle;
        }
    }

    /**
     * Prepare a query to the ContentProvider (to the database) and load the visible List of
     * messages with this data
     * This is done asynchronously.
     * This method should be called from UI thread only.
     * 
     * @param loadOneMorePage true - load one more page of messages, false - reload the same page
     */
    protected void queryListData(boolean loadOneMorePage) {
        final String method = "queryListData";
        if (!loadOneMorePage) {
            mNoMoreItems = false;
        }
        MyLog.v(this, method + (loadOneMorePage ? "loadOneMorePage" : ""));
        Bundle args = new Bundle();
        args.putBoolean(IntentExtra.LOAD_ONE_MORE_PAGE.key, loadOneMorePage);
        getLoaderManager().restartLoader(LOADER_ID, args, this);
        setLoading(method, true);
    }

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle argsIn) {
        final String method = "onCreateLoader";
        Bundle args = argsIn == null ? new Bundle() : argsIn;
        MyLog.v(this, method + " #" + id);
        args.putBoolean(IntentExtra.POSITION_RESTORED.key, isPositionRestored());

        TimelineListParameters params = TimelineListParameters.clone(
                TimelineListParameters.loadOneMorePage(args) ? mListParametersLoaded : mListParametersNew, args);
        Intent intent = getIntent();
        if (!params.mContentUri.equals(intent.getData())) {
            intent.setData(params.mContentUri);
        }
        saveSearchQuery();
        return new TimelineCursorLoader1(params);
    }

    private boolean isPositionRestored() {
        if (getListAdapter() != null) {
            return getList().isPositionRestored();
        }
        return false;
    }

    private void saveSearchQuery() {
        if (!TextUtils.isEmpty(mListParametersNew.mSearchQuery)) {
            // Record the query string in the recent queries
            // of the Suggestion Provider
            SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this,
                    TimelineSearchSuggestionsProvider.AUTHORITY, TimelineSearchSuggestionsProvider.MODE);
            suggestions.saveRecentQuery(mListParametersNew.mSearchQuery, null);

        }
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        final String method = "onLoaderReset";
        MyLog.v(this, method + " ; " + loader);
        if (getListAdapter() != null) {
            ((CursorAdapter) getListAdapter()).swapCursor(null);
        }
        setLoading(method, false);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
        final String method = "onLoadFinished";
        MyLog.v(this, method);
        boolean doChangeListContent = loader.isStarted() && cursor != null && !mFinishing;
        if (doChangeListContent && !(loader instanceof TimelineCursorLoader1)) {
            MyLog.e(this, method + "; Wrong type of loader: " + MyLog.objTagToString(loader));
            doChangeListContent = false;
        }
        TimelineCursorLoader1 myLoader = null;
        if (doChangeListContent) {
            myLoader = (TimelineCursorLoader1) loader;
            doChangeListContent = !myLoader.getParams().cancelled;
        }
        if (doChangeListContent) {
            changeListContent(myLoader, cursor);
        } else {
            setLoading(method, false);
            updateScreen();
            clearNotifications();
        }
    }

    // Parameters are not null
    private void changeListContent(final TimelineCursorLoader1 myLoader, final Cursor cursor) {
        final String method = "changeListContent1";
        TimelineFragment list = getList();
        if (list == null) {
            return;
        }

        // This check will prevent continuous loading...
        mNoMoreItems = myLoader.getParams().mIncrementallyLoadingPages
                && cursor.getCount() <= getListAdapter().getCount();
        saveListPosition();

        // This is possible from main thread only, otherwise we are getting an exception
        // The hack is to avoid the Main thread freeze: https://github.com/andstatus/andstatus/issues/183
        MySimpleCursorAdapter.beforeSwapCursor();
        ((CursorAdapter) getListAdapter()).swapCursor(cursor);
        MyLog.v(this, method + "; After changing Cursor");
        MySimpleCursorAdapter.afterSwapCursor();

        mListParametersLoaded = myLoader.getParams();
        list.restoreListPosition(mListParametersLoaded);

        boolean requestNextPage = false;
        if (!myLoader.getParams().mLoadOneMorePage && myLoader.getParams().mLastItemSentDate > 0 && cursor != null
                && cursor.getCount() < TimelineListParameters.PAGE_SIZE) {
            MyLog.v(this, method + "; Requesting next page...");
            requestNextPage = true;
        }
        if (requestNextPage) {
            queryListData(true);
        } else {
            launchReloadIfNeeded(myLoader.getParams().timelineToReload);
        }
        setLoading(method, false);
        updateScreen();
        clearNotifications();
    }

    protected TimelineFragment getList() {
        return (TimelineFragment) super.getList();
    }

    private void launchReloadIfNeeded(TimelineType timelineToReload) {
        switch (timelineToReload) {
        case ALL:
            manualReload(true, false);
            break;
        case UNKNOWN:
            break;
        default:
            manualReload(false, false);
            break;
        }
    }

    /**
     * Ask a service to load data from the Internet for the selected TimelineType
     * Only newer messages (newer than last loaded) are being loaded from the
     * Internet, older ones are not being reloaded.
     */
    protected void manualReload(boolean allTimelineTypes, boolean manuallyLaunched) {
        MyAccount ma = MyContextHolder.get().persistentAccounts().fromUserId(mListParametersNew.myAccountUserId);
        TimelineType timelineTypeForReload = TimelineType.HOME;
        long userId = 0;
        switch (mListParametersNew.getTimelineType()) {
        case DIRECT:
        case MENTIONS:
        case PUBLIC:
        case EVERYTHING:
            timelineTypeForReload = mListParametersNew.getTimelineType();
            break;
        case USER:
        case FOLLOWING_USER:
            timelineTypeForReload = mListParametersNew.getTimelineType();
            userId = mListParametersNew.mSelectedUserId;
            break;
        default:
            break;
        }
        boolean allAccounts = mListParametersNew.isTimelineCombined();
        if (userId != 0) {
            allAccounts = false;
            long originId = MyQuery.userIdToLongColumnValue(MyDatabase.User.ORIGIN_ID, userId);
            if (originId == 0) {
                MyLog.e(this, "Unknown origin for userId=" + userId);
                return;
            }
            if (!ma.isValid() || ma.getOriginId() != originId) {
                ma = MyContextHolder.get().persistentAccounts().fromUserId(userId);
                if (!ma.isValid()) {
                    ma = MyContextHolder.get().persistentAccounts().findFirstSucceededMyAccountByOriginId(originId);
                }
            }
        }
        if (!allAccounts && !ma.isValid()) {
            return;
        }

        setSyncing("manualReload", true);
        MyServiceManager.sendForegroundCommand(
                (new CommandData(CommandEnum.FETCH_TIMELINE, allAccounts ? "" : ma.getAccountName(),
                        timelineTypeForReload, userId)).setManuallyLaunched(manuallyLaunched));

        if (allTimelineTypes && ma.isValid()) {
            ma.requestSync();
        }
    }

    protected void startMyPreferenceActivity() {
        finish();
        startActivity(new Intent(this, MySettingsActivity.class));
    }

    protected void saveActivityState() {
        SharedPreferences.Editor outState = MyPreferences.getSharedPreferences(ACTIVITY_PERSISTENCE_NAME).edit();
        mListParametersNew.saveState(outState);
        mContextMenu.saveState(outState);
        outState.apply();

        final String CRASH_TEST_STRING = "Crash test 2015-04-10";
        if (MyLog.isVerboseEnabled() && mMessageEditor != null
                && mMessageEditor.getData().body.contains(CRASH_TEST_STRING)) {
            MyLog.e(this, "Initiating crash test exception");
            throw new NullPointerException("This is a test crash event");
        }

    }

    @Override
    public void startActivityForResult(Intent intent, int requestCode) {
        if (selectorActivityMock != null) {
            selectorActivityMock.startActivityForResult(intent, requestCode);
        } else {
            super.startActivityForResult(intent, requestCode);
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        MyLog.v(this, "onActivityResult; request:" + requestCode + ", result:"
                + (resultCode == RESULT_OK ? "ok" : "fail"));
        if (resultCode != RESULT_OK || data == null) {
            return;
        }
        switch (ActivityRequestCode.fromId(requestCode)) {
        case SELECT_ACCOUNT:
            accountSelected(data);
            break;
        case SELECT_ACCOUNT_TO_ACT_AS:
            accountToActAsSelected(data);
            break;
        case SELECT_ACCOUNT_TO_SHARE_VIA:
            accountToShareViaSelected(data);
            break;
        case ATTACH:
            attachmentSelected(data);
            break;
        default:
            super.onActivityResult(requestCode, resultCode, data);
            break;
        }
    }

    private void accountSelected(Intent data) {
        MyAccount ma = MyContextHolder.get().persistentAccounts()
                .fromAccountName(data.getStringExtra(IntentExtra.ACCOUNT_NAME.key));
        if (ma.isValid()) {
            MyLog.v(this, "Restarting the activity for the selected account " + ma.getAccountName());
            finish();
            TimelineType timelineTypeNew = mListParametersNew.getTimelineType();
            if (mListParametersNew.getTimelineType() == TimelineType.USER && !MyContextHolder.get()
                    .persistentAccounts().fromUserId(mListParametersNew.mSelectedUserId).isValid()) {
                /*  "Other User's timeline" vs "My User's timeline" 
                 * Actually we saw messages of the user, who is not MyAccount,
                 * so let's switch to the HOME
                 * TODO: Open "Other User's timeline" in a separate Activity?!
                 */
                timelineTypeNew = TimelineType.HOME;
            }
            MyContextHolder.get().persistentAccounts().setCurrentAccount(ma);
            mContextMenu.switchTimelineActivity(timelineTypeNew, mListParametersNew.isTimelineCombined(),
                    ma.getUserId());
        }
    }

    private void accountToActAsSelected(Intent data) {
        MyAccount ma = MyContextHolder.get().persistentAccounts()
                .fromAccountName(data.getStringExtra(IntentExtra.ACCOUNT_NAME.key));
        if (ma.isValid()) {
            mContextMenu.setAccountUserIdToActAs(ma.getUserId());
            mContextMenu.showContextMenu();
        }
    }

    private void accountToShareViaSelected(Intent data) {
        MyAccount ma = MyContextHolder.get().persistentAccounts()
                .fromAccountName(data.getStringExtra(IntentExtra.ACCOUNT_NAME.key));
        mMessageEditor.startEditingSharedData(ma, mTextToShareViaThisApp, mMediaToShareViaThisApp);
    }

    private void attachmentSelected(Intent data) {
        Uri uri = UriUtils.notNull(data.getData());
        if (!UriUtils.isEmpty(uri)) {
            UriUtils.takePersistableUriPermission(getActivity(), uri, data.getFlags());
            mMessageEditor.startEditingCurrentWithAttachedMedia(uri);
        }
    }

    /**
     * See http://stackoverflow.com/questions/960431/how-to-convert-listinteger-to-int-in-java
     */
    private static int[] toIntArray(List<Integer> list) {
        int[] ret = new int[list.size()];
        for (int i = 0; i < ret.length; i++) {
            ret[i] = list.get(i);
        }
        return ret;
    }

    @Override
    public void onReceive(CommandData commandData, MyServiceEvent event) {
        switch (event) {
        case BEFORE_EXECUTING_COMMAND:
            showSyncIndicator(commandData);
            break;
        case AFTER_EXECUTING_COMMAND:
            onReceiveAfterExecutingCommand(commandData);
            break;
        case ON_STOP:
            setSyncing("onReceive STOP", false);
            setSyncIndicator("onReceive STOP", false);
            break;
        default:
            break;
        }
    }

    private void showSyncIndicator(CommandData commandData) {
        if (!mShowSyncIndicatorOnTimeline || !isCommandToShowInSyncIndicator(commandData.getCommand())
                || mMessageEditor.isVisible()) {
            return;
        }
        setSyncIndicator("Before " + commandData.getCommand(), true);
        new AsyncTask<CommandData, Void, String>() {

            @Override
            protected String doInBackground(CommandData... commandData) {
                return commandData[0].toCommandSummary(MyContextHolder.get());
            }

            @Override
            protected void onPostExecute(String result) {
                String syncMessage = getText(R.string.title_preference_syncing) + ": " + result;
                ((TextView) findViewById(R.id.sync_text)).setText(syncMessage);
                MyLog.v(this, syncMessage);
            }

        }.execute(commandData);
    }

    private boolean isCommandToShowInSyncIndicator(CommandEnum command) {
        switch (command) {
        case AUTOMATIC_UPDATE:
        case FETCH_TIMELINE:
        case FETCH_ATTACHMENT:
        case FETCH_AVATAR:
        case UPDATE_STATUS:
        case DESTROY_STATUS:
        case CREATE_FAVORITE:
        case DESTROY_FAVORITE:
        case SEARCH_MESSAGE:
        case FOLLOW_USER:
        case STOP_FOLLOWING_USER:
        case REBLOG:
        case DESTROY_REBLOG:
            return true;
        default:
            return false;
        }
    }

    protected void onReceiveAfterExecutingCommand(CommandData commandData) {
        switch (commandData.getCommand()) {
        case FETCH_TIMELINE:
        case SEARCH_MESSAGE:
            if (commandData.isInForeground() && !commandData.isStep()) {
                setSyncing("After executing " + commandData.getCommand(), false);
            }
            break;
        case RATE_LIMIT_STATUS:
            if (commandData.getResult().getHourlyLimit() > 0) {
                mRateLimitText = commandData.getResult().getRemainingHits() + "/"
                        + commandData.getResult().getHourlyLimit();
                updateTitle();
            }
            break;
        case UPDATE_STATUS:
            mMessageEditor.loadCurrentDraft();
            break;
        default:
            break;
        }
        if (mShowSyncIndicatorOnTimeline && isCommandToShowInSyncIndicator(commandData.getCommand())) {
            ((TextView) findViewById(R.id.sync_text)).setText("");
        }
    }

    private void setLoading(String source, boolean isLoading) {
        TimelineFragment list = getList();
        if (list != null && list.isLoading() != isLoading && !isFinishing()) {
            list.setLoading(source, isLoading);
        }
    }

    private void setSyncing(String source, boolean isSyncing) {
        if (mSwipeRefreshLayout != null && mSwipeRefreshLayout.isRefreshing() != isSyncing && !isFinishing()) {
            MyLog.v(this, source + " set Syncing to " + isSyncing);
            mSwipeRefreshLayout.setRefreshing(isSyncing);
        }
    }

    @Override
    public MyBaseListActivity getActivity() {
        return this;
    }

    @Override
    public MessageEditor getMessageEditor() {
        return mMessageEditor;
    }

    @Override
    public void onMessageEditorVisibilityChange(boolean isVisible) {
        setSyncIndicator("onMessageEditorVisibilityChange", false);
        invalidateOptionsMenu();
    }

    @Override
    public long getCurrentMyAccountUserId() {
        return mListParametersNew.myAccountUserId;
    }

    @Override
    public TimelineType getTimelineType() {
        return mListParametersNew.getTimelineType();
    }

    @Override
    public boolean isTimelineCombined() {
        return mListParametersNew.isTimelineCombined();
    }

    @Override
    public long getSelectedUserId() {
        return mListParametersNew.mSelectedUserId;
    }
}