Java tutorial
/* * 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; import android.app.ActionBar; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.ListActivity; 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.database.MatrixCursor; 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.ActionBarDrawerToggle; import android.text.TextUtils; import android.view.LayoutInflater; 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.AbsListView.OnScrollListener; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.CheckBox; import android.widget.CursorAdapter; import android.widget.LinearLayout; import android.widget.TextView; 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.MyDatabase; import org.andstatus.app.data.MyDatabase.User; import org.andstatus.app.data.MyProvider; import org.andstatus.app.data.TimelineSearchSuggestionsProvider; import org.andstatus.app.data.TimelineSql; import org.andstatus.app.data.TimelineTypeEnum; import org.andstatus.app.data.TimelineViewBinder; import org.andstatus.app.service.CommandData; import org.andstatus.app.service.CommandEnum; import org.andstatus.app.service.MyServiceEvent; import org.andstatus.app.service.MyServiceListener; import org.andstatus.app.service.MyServiceManager; import org.andstatus.app.service.MyServiceReceiver; import org.andstatus.app.service.QueueViewer; 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.ArrayList; import java.util.List; /** * @author yvolk@yurivolkov.com */ public class TimelineActivity extends ListActivity implements MyServiceListener, OnScrollListener, OnItemClickListener, 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(); /** * Visibility of the layout indicates whether Messages are being loaded into the list (asynchronously...) * The layout appears at the bottom of the list of messages * when new items are being loaded into the list */ private LinearLayout mLoadingLayout; private MySwipeRefreshLayout mSwipeRefreshLayout = null; /** Parameters of currently shown Timeline */ private TimelineListParameters mListParameters; private TimelineListParameters mListParametersNew; /** * Is saved position restored (or some default positions set)? */ private boolean mPositionRestored = false; /** * 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; MyServiceReceiver 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; private boolean mIsLoading = false; /** * Time when shared preferences where changed */ private long mPreferencesChangeTime = 0; private MessageContextMenu mContextMenu; private MessageEditor mMessageEditor; private Menu mOptionsMenu = null; private String mTextToShareViaThisApp = ""; private Uri mMediaToShareViaThisApp = Uri.EMPTY; private String mRateLimitText = ""; DrawerLayout mDrawerLayout; ActionBarDrawerToggle mDrawerToggle; /** * 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) { super.onCreate(savedInstanceState); mListParameters = 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")); } if (HelpActivity.startFromActivity(this)) { return; } mListParametersNew.myAccountUserId = MyContextHolder.get().persistentAccounts().getCurrentAccountUserId(); mServiceConnector = new MyServiceReceiver(this); MyPreferences.setThemedContentView(this, R.layout.timeline); mSyncIndicator = findViewById(R.id.sync_indicator); mContextMenu = new MessageContextMenu(this); mMessageEditor = new MessageEditor(this); initializeSwipeRefresh(); restoreActivityState(); LayoutInflater inflater = LayoutInflater.from(this); // Create list footer to show the progress of message loading mLoadingLayout = (LinearLayout) inflater.inflate(R.layout.item_loading, null); getListView().addFooterView(mLoadingLayout); createListAdapter(getEmptyCursor()); // Attach listeners to the message list getListView().setOnScrollListener(this); getListView().setOnCreateContextMenuListener(mContextMenu); getListView().setOnItemClickListener(this); initializeDrawer(); getLoaderManager().initLoader(LOADER_ID, null, this); if (savedInstanceState == null) { parseNewIntent(getIntent()); } updateScreen(); queryListData(false); } private void initializeSwipeRefresh() { mSwipeRefreshLayout = (MySwipeRefreshLayout) findViewById(R.id.swipeRefreshLayout); 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); getActionBar().setDisplayHomeAsUpEnabled(true); getActionBar().setHomeButtonEnabled(true); } private Cursor getEmptyCursor() { return new MatrixCursor(TimelineSql.getTimelineProjection()); } private boolean restoreActivityState() { SharedPreferences activityState = MyPreferences.getSharedPreferences(ACTIVITY_PERSISTENCE_NAME); boolean stateRestored = false; if (activityState != null) { stateRestored = mListParametersNew.restoreState(activityState); if (stateRestored) { mMessageEditor.loadState(activityState); mContextMenu.loadState(activityState); } } return stateRestored; } /** * View.OnClickListener */ public boolean 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); return true; } private void closeDrawer() { ViewGroup mDrawerList = (ViewGroup) findViewById(R.id.navigation_drawer); mDrawerLayout.closeDrawer(mDrawerList); } /** * View.OnClickListener */ public boolean onTimelineTypeButtonClick(View item) { showDialog(DIALOG_ID_TIMELINE_TYPE); closeDrawer(); return true; } /** * View.OnClickListener */ public boolean onSelectAccountButtonClick(View item) { if (MyContextHolder.get().persistentAccounts().size() > 1) { AccountSelector.selectAccount(TimelineActivity.this, 0, ActivityRequestCode.SELECT_ACCOUNT); } closeDrawer(); return true; } /** * 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.EXTRA_TIMELINE_TYPE.key, appGlobalSearch ? TimelineTypeEnum.EVERYTHING.save() : mListParametersNew.getTimelineType().save()); appSearchData.putBoolean(IntentExtra.EXTRA_TIMELINE_IS_COMBINED.key, mListParametersNew.isTimelineCombined()); appSearchData.putLong(IntentExtra.EXTRA_SELECTEDUSERID.key, mListParametersNew.mSelectedUserId); appSearchData.putBoolean(IntentExtra.EXTRA_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); } } private void saveListPosition() { new TimelineListPositionStorage(getListAdapter(), getListView(), mListParameters).save(); } @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"; super.onPause(); if (MyLog.isLoggable(this, MyLog.VERBOSE)) { MyLog.v(this, method + "; instanceId=" + mInstanceId); } mServiceConnector.unregisterReceiver(this); setSyncIndicator(method, false); if (mPositionRestored) { setFastScrollThumb(false); if (!isLoading()) { saveListPosition(); } mPositionRestored = false; } saveActivityState(); 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); } } private void setFastScrollThumb(boolean isVisible) { getListView().setFastScrollEnabled(isVisible); } /** * 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; } runOnUiThread(new Runnable() { @Override public void run() { if (mPositionRestored) { saveListPosition(); } } }); super.finish(); } @Override protected Dialog onCreateDialog(int id) { switch (id) { case DIALOG_ID_TIMELINE_TYPE: return newTimelinetypeSelector(); default: break; } return super.onCreateDialog(id); } 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 TimelineTypeEnum type = selector.positionToType(which); if (type != TimelineTypeEnum.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); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { mOptionsMenu = menu; MyAccount ma = MyContextHolder.get().persistentAccounts().getCurrentAccount(); boolean enableReload = isTimelineCombined() || ma.isValidAndVerified(); 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); } /** * Listener that checks for clicks on the main list view. * * @param adapterView * @param view * @param position * @param id */ @Override public void onItemClick(AdapterView<?> adapterView, final View view, final int position, final long id) { if (id <= 0) { if (MyLog.isLoggable(this, MyLog.VERBOSE)) { 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().getAccountWhichMayBeLinkedToThisMessage( id, linkedUserId, mListParametersNew.myAccountUserId); if (MyLog.isLoggable(this, MyLog.VERBOSE)) { MyLog.v(this, "onItemClick, position=" + position + "; id=" + id + "; view=" + view + "; linkedUserId=" + linkedUserId + " account=" + ma.getAccountName()); } return MyProvider.getTimelineMsgUri(ma.getUserId(), mListParametersNew.getTimelineType(), true, 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(new Intent(Intent.ACTION_VIEW, 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; } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { if (!mNoMoreItems && mPositionRestored && !isLoading()) { // 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); } } } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { switch (scrollState) { case OnScrollListener.SCROLL_STATE_IDLE: break; case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL: break; case OnScrollListener.SCROLL_STATE_FLING: setFastScrollThumb(true); break; default: break; } } 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.isValidAndVerified()) { accountButtonText = "(" + accountButtonText + ")"; } return accountButtonText; } @Override protected void onNewIntent(Intent intent) { if (MyLog.isLoggable(this, MyLog.VERBOSE)) { 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) { mRateLimitText = ""; mListParametersNew.setTimelineType(TimelineTypeEnum.UNKNOWN); mListParametersNew.myAccountUserId = MyContextHolder.get().persistentAccounts().getCurrentAccountUserId(); mListParametersNew.mSelectedUserId = 0; parseAppSearchData(intentNew); if (mListParametersNew.getTimelineType() == TimelineTypeEnum.UNKNOWN) { mListParametersNew.parseIntentData(intentNew); } if (mListParametersNew.getTimelineType() == TimelineTypeEnum.UNKNOWN) { /* Set default values */ mListParametersNew.setTimelineType(TimelineTypeEnum.HOME); mListParametersNew.mSearchQuery = ""; } if (mListParametersNew.getTimelineType() == TimelineTypeEnum.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) { // We use other packaging of the same parameters in onSearchRequested mListParametersNew.setTimelineType( TimelineTypeEnum.load(appSearchData.getString(IntentExtra.EXTRA_TIMELINE_TYPE.key))); if (mListParametersNew.getTimelineType() != TimelineTypeEnum.UNKNOWN) { mListParametersNew.setTimelineCombined(appSearchData.getBoolean( IntentExtra.EXTRA_TIMELINE_IS_COMBINED.key, mListParametersNew.isTimelineCombined())); /* The query itself is still from the Intent */ mListParametersNew.mSearchQuery = TimelineListParameters .notNullString(intentNew.getStringExtra(SearchManager.QUERY)); mListParametersNew.mSelectedUserId = appSearchData.getLong(IntentExtra.EXTRA_SELECTEDUSERID.key, mListParametersNew.mSelectedUserId); if (!TextUtils.isEmpty(mListParametersNew.mSearchQuery) && appSearchData.getBoolean(IntentExtra.EXTRA_GLOBAL_SEARCH.key, false)) { setSyncing("Global search: " + mListParametersNew.mSearchQuery, true); MyServiceManager .sendForegroundCommand( 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(mListParameters.getTimelineType() != TimelineTypeEnum.UNKNOWN ? mListParameters : mListParametersNew, mRateLimitText).updateTitle(this); } static class TimelineTitle { StringBuilder title = new StringBuilder(); 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() == TimelineTypeEnum.USER && !(ta.isTimelineCombined() && MyContextHolder.get().persistentAccounts().fromUserId(ta.getSelectedUserId()).isValid())) { I18n.appendWithSpace(title, MyProvider.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(Activity activity) { ActionBar actionBar = activity.getActionBar(); if (actionBar != null) { actionBar.setTitle(title); actionBar.setSubtitle(subTitle); } if (MyLog.isLoggable(activity, MyLog.VERBOSE)) { 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.EXTRA_LOAD_ONE_MORE_PAGE.key, loadOneMorePage); args.putInt(IntentExtra.EXTRA_ROWS_LIMIT.key, calcRowsLimit(loadOneMorePage)); getLoaderManager().restartLoader(LOADER_ID, args, this); setLoading(method, true); } private int calcRowsLimit(boolean loadOneMorePage) { int nMessages = 0; if (getListAdapter() != null) { Cursor cursor = ((CursorAdapter) getListAdapter()).getCursor(); if (cursor != null && !cursor.isClosed()) { nMessages = cursor.getCount(); } } if (loadOneMorePage) { nMessages += TimelineListParameters.PAGE_SIZE; } else if (nMessages < TimelineListParameters.PAGE_SIZE) { nMessages = TimelineListParameters.PAGE_SIZE; } return nMessages; } @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.EXTRA_POSITION_RESTORED.key, mPositionRestored && (getListAdapter() != null)); TimelineListParameters params = TimelineListParameters.clone(mListParametersNew, args); Intent intent = getIntent(); if (!params.mContentUri.equals(intent.getData())) { intent.setData(params.mContentUri); } saveSearchQuery(); return new TimelineCursorLoader1(params); } 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"; // 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(); mListParameters = myLoader.getParams(); mPositionRestored = new TimelineListPositionStorage(getListAdapter(), getListView(), mListParameters) .restoreListPosition(); 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(); } private void launchReloadIfNeeded(TimelineTypeEnum 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 manuallyLauched) { MyAccount ma = MyContextHolder.get().persistentAccounts().fromUserId(mListParametersNew.myAccountUserId); TimelineTypeEnum timelineTypeForReload = TimelineTypeEnum.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 = MyProvider.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(manuallyLauched)); 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); mMessageEditor.saveState(outState); mContextMenu.saveState(outState); outState.commit(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { 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.EXTRA_ACCOUNT_NAME.key)); if (ma.isValid()) { MyLog.v(this, "Restarting the activity for the selected account " + ma.getAccountName()); finish(); TimelineTypeEnum timelineTypeNew = mListParametersNew.getTimelineType(); if (mListParametersNew.getTimelineType() == TimelineTypeEnum.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 = TimelineTypeEnum.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.EXTRA_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.EXTRA_ACCOUNT_NAME.key)); mMessageEditor.startEditingMessage(new MessageEditorData(ma).setMessageText(mTextToShareViaThisApp) .setMediaUri(mMediaToShareViaThisApp)); } private void attachmentSelected(Intent data) { Uri uri = UriUtils.notNull(data.getData()); if (!UriUtils.isEmpty(uri)) { mMediaToShareViaThisApp = uri; if (mMessageEditor.isVisible()) { mMessageEditor.setMedia(mMediaToShareViaThisApp); } } } private void createListAdapter(Cursor cursor) { List<String> columnNames = new ArrayList<String>(); List<Integer> viewIds = new ArrayList<Integer>(); columnNames.add(MyDatabase.User.AUTHOR_NAME); viewIds.add(R.id.message_author); columnNames.add(MyDatabase.Msg.BODY); viewIds.add(R.id.message_body); columnNames.add(MyDatabase.Msg.CREATED_DATE); viewIds.add(R.id.message_details); columnNames.add(MyDatabase.MsgOfUser.FAVORITED); viewIds.add(R.id.message_favorited); columnNames.add(MyDatabase.Msg._ID); viewIds.add(R.id.id); int listItemLayoutId = R.layout.message_basic; if (MyPreferences.showAvatars()) { listItemLayoutId = R.layout.message_avatar; columnNames.add(MyDatabase.Download.AVATAR_FILE_NAME); viewIds.add(R.id.avatar_image); } if (MyPreferences.showAttachedImages()) { columnNames.add(MyDatabase.Download.IMAGE_ID); viewIds.add(R.id.attached_image); } MySimpleCursorAdapter mCursorAdapter = new MySimpleCursorAdapter(TimelineActivity.this, listItemLayoutId, cursor, columnNames.toArray(new String[] {}), toIntArray(viewIds), 0); mCursorAdapter.setViewBinder(new TimelineViewBinder()); setListAdapter(mCursorAdapter); } /** * 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; } } private 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; default: break; } if (mShowSyncIndicatorOnTimeline && isCommandToShowInSyncIndicator(commandData.getCommand())) { ((TextView) findViewById(R.id.sync_text)).setText(""); } } private boolean isLoading() { return mIsLoading; } private void setLoading(String source, boolean isLoading) { if (isLoading() != isLoading && !isFinishing()) { mIsLoading = isLoading; MyLog.v(this, source + " set isLoading to " + isLoading); mLoadingLayout.setVisibility(isLoading ? View.VISIBLE : View.INVISIBLE); } } 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); } } protected Menu getOptionsMenu() { return mOptionsMenu; } @Override public Activity 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 TimelineTypeEnum getTimelineType() { return mListParametersNew.getTimelineType(); } @Override public boolean isTimelineCombined() { return mListParametersNew.isTimelineCombined(); } @Override public long getSelectedUserId() { return mListParametersNew.mSelectedUserId; } }