package com.zulip.android.activities;
import android.Manifest;
import android.animation.Animator;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.app.DatePickerDialog;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.SearchManager;
import android.content.BroadcastReceiver;
import android.content.ClipData;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MergeCursor;
import android.graphics.Bitmap;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.os.Environment;
import android.os.Handler;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.customtabs.CustomTabsIntent;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.ActionBarDrawerToggle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.FileProvider;
import android.support.v4.view.GravityCompat;
import android.support.v4.view.MenuItemCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.support.v4.widget.DrawerLayout;
import android.support.v4.widget.SimpleCursorAdapter;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatDelegate;
import android.support.v7.app.NotificationCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewPropertyAnimator;
import android.view.WindowManager;
import android.view.animation.Interpolator;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AutoCompleteTextView;
import android.widget.DatePicker;
import android.widget.EditText;
import android.widget.ExpandableListView;
import android.widget.FilterQueryProvider;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.SimpleCursorTreeAdapter;
import android.widget.TextView;
import android.widget.Toast;
import com.j256.ormlite.android.AndroidDatabaseResults;
import com.zulip.android.BuildConfig;
import com.zulip.android.R;
import com.zulip.android.ZulipApp;
import com.zulip.android.database.DatabaseHelper;
import com.zulip.android.filters.NarrowFilter;
import com.zulip.android.filters.NarrowFilterAllPMs;
import com.zulip.android.filters.NarrowFilterByDate;
import com.zulip.android.filters.NarrowFilterPM;
import com.zulip.android.filters.NarrowFilterSearch;
import com.zulip.android.filters.NarrowFilterStar;
import com.zulip.android.filters.NarrowFilterStream;
import com.zulip.android.filters.NarrowListener;
import com.zulip.android.gcm.GcmBroadcastReceiver;
import com.zulip.android.gcm.Notifications;
import com.zulip.android.models.Emoji;
import com.zulip.android.models.Message;
import com.zulip.android.models.MessageType;
import com.zulip.android.models.Person;
import com.zulip.android.models.Presence;
import com.zulip.android.models.PresenceType;
import com.zulip.android.models.Stream;
import com.zulip.android.networking.AsyncGetEvents;
import com.zulip.android.networking.AsyncSend;
import com.zulip.android.networking.AsyncStatusUpdate;
import com.zulip.android.networking.UploadProgressRequest;
import com.zulip.android.networking.ZulipAsyncPushTask;
import com.zulip.android.networking.response.UploadResponse;
import com.zulip.android.networking.util.DefaultCallback;
import com.zulip.android.util.ActivityTransitionAnim;
import com.zulip.android.util.AnimationHelper;
import com.zulip.android.util.CommonProgressDialog;
import com.zulip.android.util.Constants;
import com.zulip.android.util.FileUtils;
import com.zulip.android.util.ListDialog;
import com.zulip.android.util.MutedTopics;
import com.zulip.android.util.RemoveViewsOnScroll;
import com.zulip.android.util.SwipeRemoveLinearLayout;
import com.zulip.android.util.UrlHelper;
import com.zulip.android.util.ZLog;
import com.zulip.android.viewholders.TopSnackBar;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Callable;
import okhttp3.MultipartBody;
import retrofit2.Call;
import retrofit2.Response;
import static com.zulip.android.util.Constants.REQUEST_PICK_FILE;
/**
* The main Activity responsible for holding the {@link MessageListFragment} which has the list to the
* messages
*/
public class ZulipActivity extends BaseActivity implements
MessageListFragment.Listener, NarrowListener, SwipeRemoveLinearLayout.leftToRightSwipeListener,
ListDialog.ListDialogListener {
private static final String NARROW = "narrow";
private static final String PARAMS = "params";
//At these many letters the emoji/person hint will not show now on
private static final int MAX_THRESOLD_EMOJI_HINT = 5;
//At these many letters the emoji/person hint starts to show up
private static final int MIN_THRESOLD_EMOJI_HINT = 1;
private static final int PERMISSION_REQUEST_READ_STORAGE = 1;
private static final int REQUEST_TAKE_PHOTO = 2;
private static final Interpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator();
private static final int HIDE_FAB_AFTER_SEC = 5;
// row number which is used to differentiate the 'All private messages'
// row from the people
final int allPeopleId = -1;
public MessageListFragment currentList;
public CommonProgressDialog commonProgressDialog;
FloatingActionButton fab;
NarrowFilter narrowFilter;
String prevId = null;
int prevMessageSameCount = -1;
private ZulipApp app;
private boolean logged_in = false;
private boolean backPressedOnce = false;
private boolean inSearch = false;
private ZulipActivity that = this; // self-ref
private DrawerLayout drawerLayout;
private ActionBarDrawerToggle drawerToggle;
private SwipeRemoveLinearLayout chatBox;
private CountDownTimer fabHidder;
private boolean isTextFieldFocused = false;
private HashMap<String, Bitmap> gravatars = new HashMap<>();
private AsyncGetEvents event_poll;
private Handler statusUpdateHandler;
private Runnable statusUpdateRunnable;
private int mToolbarHeightInPx;
private int statusBarHeight = 0;
private MessageListFragment narrowedList;
private MessageListFragment homeList;
private AutoCompleteTextView streamActv;
private AutoCompleteTextView topicActv;
private AutoCompleteTextView messageEt;
private TextView textView;
private ImageView sendBtn;
private ImageView togglePrivateStreamBtn;
private android.support.v7.widget.SearchView searchView;
private Notifications notifications;
private SimpleCursorAdapter streamActvAdapter;
private SimpleCursorAdapter subjectActvAdapter;
private SimpleCursorAdapter emailActvAdapter;
private AppBarLayout appBarLayout;
private MutedTopics mMutedTopics;
private BroadcastReceiver onGcmMessage = new BroadcastReceiver() {
public void onReceive(Context contenxt, Intent intent) {
// Block the event before it propagates to show a notification.
// TODO: could be smarter and only block the event if the message is
// in the narrow.
Log.i("GCM", "Dropping a push because the activity is active");
abortBroadcast();
}
};
private ExpandableStreamDrawerAdapter streamsDrawerAdapter;
private Uri mFileUri;
private Uri mImageUri;
private ImageView cameraBtn;
private String mCurrentPhotoPath;
private Menu menu;
private Calendar calendar;
private EditText etSearchPeople;
private ImageView ivSearchPeopleCancel;
private EditText etSearchStream;
private ImageView ivSearchStreamCancel;
private ListView peopleDrawer;
private Toast toast;
private ImageView addFileBtn;
//
private String streamSearchFilterKeyword = "";
private RecyclerView.ViewHolder viewHolder;
private boolean isActionBarShown = true;
private SimpleCursorAdapter.ViewBinder peopleBinder = new SimpleCursorAdapter.ViewBinder() {
@Override
public boolean setViewValue(View view, Cursor cursor, int i) {
switch (view.getId()) {
case R.id.name:
TextView name = (TextView) view;
name.setText(cursor.getString(i));
return true;
case R.id.stream_dot:
String email = cursor.getString(i);
if (app == null || email == null) {
view.setVisibility(View.INVISIBLE);
} else {
Presence presence = app.presences.get(email);
if (presence == null) {
view.setVisibility(View.INVISIBLE);
} else {
PresenceType status = presence.getStatus();
long age = presence.getAge();
if (age > 2 * 60) {
view.setVisibility(View.VISIBLE);
view.setBackgroundResource(R.drawable.presence_inactive);
} else if (PresenceType.ACTIVE == status) {
view.setVisibility(View.VISIBLE);
view.setBackgroundResource(R.drawable.presence_active);
} else if (PresenceType.IDLE == status) {
view.setVisibility(View.VISIBLE);
view.setBackgroundResource(R.drawable.presence_away);
} else {
view.setVisibility(View.INVISIBLE);
}
}
}
return true;
default:
break;
}
return false;
}
};
private RefreshableCursorAdapter peopleAdapter;
private LinearLayout composeStatus;
private String tempStreamSave = null;
private TopSnackBar topSnackBar;
private NotificationManager mNotificationManager;
private NotificationCompat.Builder mBuilder;
private HashMap<Integer, Call<UploadResponse>> mCancelHashMap;
private ActionBar actionBar;
@Override
public void removeChatBox(boolean animToRight) {
if (!searchView.findViewById(android.support.v7.appcompat.R.id.search_src_text).hasFocus()) {
hideSoftKeyBoard();
}
AnimationHelper.hideViewX(chatBox, animToRight);
//show fab button
CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) fab.getLayoutParams();
RemoveViewsOnScroll removeViewsOnScroll = (RemoveViewsOnScroll) layoutParams.getBehavior();
removeViewsOnScroll.showView(fab);
}
public HashMap<String, Bitmap> getGravatars() {
return gravatars;
}
@Override
public void recyclerViewScrolled(boolean isReachedAtBottom) {
/* in this method we check if the messageEt is empty or not
if messageEt is not empty, it means that the user has typed something in the chatBox and that the chatBox should be open
in spite of scrolling */
if (chatBox.getVisibility() == View.VISIBLE && !isCurrentModeStream()) {
//if messageEt is empty in private msg mode, then the chatBox can disappear on scrolling else it will stay
if (messageEt.getText().toString().equals("")) {
displayChatBox(false);
displayFAB(true);
}
} else if (chatBox.getVisibility() == View.VISIBLE && isCurrentModeStream()) {
//check if messageEt is empty in stream msg mode, then the chatBox can disappear on scrolling else it will disappear
if (messageEt.getText().toString().equals("") && topicActv.getText().toString().equals("")) {
displayChatBox(false);
displayFAB(true);
}
}
/*check if stream edittext, topic edittext and messageEt edittext is empty in a general msg mode(i.e. when the floating
button is pressed by user). If all fields are empty, then on scrolling the chatBox will disappear else not */
else if (chatBox.getVisibility() == View.VISIBLE && streamActv.getText().toString().equals("") && topicActv.getText().toString().equals("") && messageEt.getText().toString().equals("")) {
displayChatBox(false);
displayFAB(true);
}
//on reaching at bottom dismiss snackBar
if (isReachedAtBottom && topSnackBar.isShown()) {
topSnackBar.dismiss();
}
}
public RefreshableCursorAdapter getPeopleAdapter() {
return peopleAdapter;
}
/**
* Called when the activity is first created.
*/
@SuppressLint("NewApi")
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
app = (ZulipApp) getApplicationContext();
processParams();
if (!app.isLoggedIn()) {
openLogin(null);
return;
}
if (mMutedTopics == null) {
mMutedTopics = MutedTopics.get();
}
this.logged_in = true;
notifications = new Notifications(this);
notifications.register();
setContentView(R.layout.main);
commonProgressDialog = new CommonProgressDialog(this);
final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
toolbar.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String message = String.valueOf(toolbar.getTitle());
if (toolbar.getSubtitle() != null) {
message += " \u203a " + toolbar.getSubtitle();
}
Toast.makeText(ZulipActivity.this, message, Toast.LENGTH_SHORT).show();
}
});
setSupportActionBar(toolbar);
actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeAsUpIndicator(R.drawable.ic_menu_24dp);
actionBar.setHomeButtonEnabled(true);
actionBar.setTitle(R.string.app_name);
}
streamActv = (AutoCompleteTextView) findViewById(R.id.stream_actv);
topicActv = (AutoCompleteTextView) findViewById(R.id.topic_actv);
messageEt = (AutoCompleteTextView) findViewById(R.id.message_et);
textView = (TextView) findViewById(R.id.textView);
sendBtn = (ImageView) findViewById(R.id.send_btn);
addFileBtn = (ImageView) findViewById(R.id.add_btn);
appBarLayout = (AppBarLayout) findViewById(R.id.appBarLayout);
boolean isCurrentThemeNight = (AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES);
etSearchPeople = (EditText) findViewById(R.id.people_drawer_search);
ivSearchPeopleCancel = (ImageView) findViewById(R.id.iv_people__search_cancel_button);
onTextChangeOfPeopleSearchEditText();
mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mBuilder = (NotificationCompat.Builder) new NotificationCompat.Builder(this)
.setColor(ContextCompat.getColor(this, R.color.notif_background));
ivSearchPeopleCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//set default people list
resetPeopleSearch();
}
});
etSearchStream = (EditText) findViewById(R.id.stream_drawer_search);
if (isCurrentThemeNight) {
etSearchPeople.setTextColor(ContextCompat.getColor(this, R.color.color_text_black));
etSearchStream.setTextColor(ContextCompat.getColor(this, R.color.color_text_black));
}
ivSearchStreamCancel = (ImageView) findViewById(R.id.iv_stream_search_cancel_button);
onTextChangeOfStreamSearchEditText();
ivSearchStreamCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//set default stream list
resetStreamSearch();
}
});
app.setZulipActivity(this);
togglePrivateStreamBtn = (ImageView) findViewById(R.id.togglePrivateStream_btn);
drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
drawerToggle = new ActionBarDrawerToggle(this, drawerLayout,
R.drawable.ic_drawer, R.string.streams_open,
R.string.streams_close) {
/** Called when a drawer has settled in a completely closed state. */
public void onDrawerClosed(View view) {
// pass
}
/** Called when a drawer has settled in a completely open state. */
public void onDrawerOpened(View drawerView) {
// pass
try {
streamsDrawerAdapter.changeCursor(getSteamCursorGenerator().call());
} catch (Exception e) {
ZLog.logException(e);
}
streamsDrawerAdapter.notifyDataSetChanged();
}
};
// Set the drawer toggle as the DrawerListener
drawerLayout.setDrawerListener(drawerToggle);
peopleDrawer = (ListView) findViewById(R.id.people_drawer);
//set up people list
setUpPeopleList();
peopleDrawer.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
resetPeopleSearch();
if (id == allPeopleId) {
doNarrow(new NarrowFilterAllPMs(app.getYou()));
} else {
Person person = Person.getById(app, (int) id);
narrowPMWith(person);
switchToPrivate();
}
}
});
// send status update and check again every couple minutes
statusUpdateHandler = new Handler();
statusUpdateRunnable = new Runnable() {
@Override
public void run() {
AsyncStatusUpdate task = new AsyncStatusUpdate(
ZulipActivity.this);
task.setCallback(new ZulipAsyncPushTask.AsyncTaskCompleteListener() {
@Override
public void onTaskComplete(String result, JSONObject object) {
peopleAdapter.refresh();
}
@Override
public void onTaskFailure(String result) {
}
});
task.execute();
statusUpdateHandler.postDelayed(this, 2 * 60 * 1000);
}
};
statusUpdateHandler.post(statusUpdateRunnable);
homeList = MessageListFragment.newInstance(null);
pushListFragment(homeList, null);
togglePrivateStreamBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
switchView();
}
});
sendBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendMessage();
}
});
/**
* set click listener on add file button to open custom list dialog {@link ListDialog}
*/
addFileBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showListDialog();
}
});
composeStatus = (LinearLayout) findViewById(R.id.composeStatus);
setUpAdapter();
streamActv.setAdapter(streamActvAdapter);
topicActv.setAdapter(subjectActvAdapter);
checkAndSetupStreamsDrawer();
setupFab();
View.OnFocusChangeListener focusChangeListener = new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View view, boolean focus) {
isTextFieldFocused = focus;
}
};
messageEt.setOnFocusChangeListener(focusChangeListener);
topicActv.setOnFocusChangeListener(focusChangeListener);
streamActv.setOnFocusChangeListener(focusChangeListener);
SimpleCursorAdapter combinedAdapter = new SimpleCursorAdapter(
that, R.layout.emoji_tile, null,
new String[]{Emoji.NAME_FIELD, Emoji.NAME_FIELD},
new int[]{R.id.emojiImageView, R.id.nameTV}, 0);
combinedAdapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() {
@Override
public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
//TODO - columnIndex is 6 for Person table and columnIndex is 1 for Emoji table, Confirm this will be perfect to distinguish between these two tables! It seems alphabetical ordering of columns!
boolean personTable = !(columnIndex == 1);
String name = cursor.getString(cursor.getColumnIndex(Emoji.NAME_FIELD));
switch (view.getId()) {
case R.id.emojiImageView:
if (personTable) {
view.setVisibility(View.GONE);
} else {
try {
Drawable drawable = Drawable.createFromStream(getApplicationContext().getAssets().open("emoji/" + name + ".png"),
"emoji/" + name + ".png");
((ImageView) view).setImageDrawable(drawable);
} catch (Exception e) {
ZLog.logException(e);
}
}
return true;
case R.id.nameTV:
((TextView) view).setText(name);
return true;
}
if (BuildConfig.DEBUG)
ZLog.logException(new RuntimeException(getResources().getResourceName(view.getId()) + " - this view not binded!"));
return false;
}
});
combinedAdapter.setCursorToStringConverter(new SimpleCursorAdapter.CursorToStringConverter() {
@Override
public CharSequence convertToString(Cursor cursor) {
if (cursor == null) return messageEt.getText();
int index = cursor.getColumnIndex(Emoji.NAME_FIELD);
String name = cursor.getString(index);
String currText = messageEt.getText().toString();
int numberOfColumns = cursor.getColumnCount();
int last = (numberOfColumns > 2) ? currText.lastIndexOf("@") : currText.lastIndexOf(":");
return TextUtils.substring(currText, 0, last) + ((numberOfColumns > 2) ? "@**" + name + "**" : ":" + name.replace(".png", "") + ":");
}
});
combinedAdapter.setFilterQueryProvider(new FilterQueryProvider() {
@Override
public Cursor runQuery(CharSequence charSequence) {
if (charSequence == null) return null;
int length = charSequence.length();
int personLength = charSequence.toString().lastIndexOf("@");
int smileyLength = charSequence.toString().lastIndexOf(":");
if (length - 1 > Math.max(personLength, smileyLength) + MAX_THRESOLD_EMOJI_HINT
|| length - Math.max(personLength, smileyLength) - 1 < MIN_THRESOLD_EMOJI_HINT
|| (personLength + smileyLength == -2))
return null;
try {
if (personLength > smileyLength) {
return makePeopleNameCursor(charSequence.subSequence(personLength + 1, length));
} else {
return makeEmojiCursor(charSequence.subSequence(smileyLength + 1, length));
}
} catch (SQLException e) {
Log.e("SQLException", "SQL not correct", e);
return null;
}
}
});
messageEt.setAdapter(combinedAdapter);
// Get intent, action and MIME type
Intent intent = getIntent();
String action = intent.getAction();
String type = intent.getType();
if (Intent.ACTION_SEND.equals(action) && type != null) {
if ("text/plain".equals(type)) {
// Handle text being sent
handleSentText(intent);
} else {
// Handle single file being sent
handleSentFile((Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM));
}
}
handleOnFragmentChange();
calendar = Calendar.getInstance();
setupSnackBar();
//Hides Keyboard if it was open with focus on an editText before restart of the activity
this.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
}
public void showListDialog() {
// Create an instance of the dialog fragment and show it
DialogFragment dialog = new ListDialog();
dialog.show(getSupportFragmentManager(), "ListDialogFragment");
}
/**
* The dialog fragment receives a reference to this Activity through the
* Fragment.onAttach() callback, which it uses to call the following methods
* defined by the ListDialogFragment.ListDialogListener interface.
*/
@Override
public void onDialogPhotoClick(DialogFragment dialog) {
// User touched the dialog's "Take picture" button
dialog.dismiss();
dispatchTakePictureIntent();
}
@Override
public void onDialogFileClick(DialogFragment dialog) {
// User touched the dialog's "Pick a file" button
dialog.dismiss();
dispatchPickIntent();
}
/**
* Called when fragment is changed
* When narrowedList == null means home page show Today in menu
* When narrowedList.filter instanceof NarrowFilterByDate show One Day before in menu
*/
private void handleOnFragmentChange() {
getSupportFragmentManager().addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {
@Override
public void onBackStackChanged() {
if (menu == null)
return;
if (narrowedList == null) {
calendar = Calendar.getInstance();
menu.getItem(2).getSubMenu().getItem(0).setTitle(R.string.menu_today);
switchToStream();
checkForChatBoxFocusRequest();
} else if (narrowedList.filter instanceof NarrowFilterByDate) {
menu.getItem(2).getSubMenu().getItem(0).setTitle(R.string.menu_one_day_before);
}
}
});
}
private void checkForChatBoxFocusRequest() {
if (TextUtils.isEmpty(streamActv.getText().toString()) && isCurrentModeStream()) {
streamActv.requestFocus();
} else if (TextUtils.isEmpty(topicActv.getText().toString())) {
topicActv.requestFocus();
} else {
messageEt.requestFocus();
}
}
/**
* Change peopleAdapter cursor to default
* Clear text of etSearchPeople
* Remove focus of etSearchPeople
*/
private void resetPeopleSearch() {
try {
peopleAdapter.changeCursor(getPeopleCursorGenerator().call());
} catch (Exception e) {
ZLog.logException(e);
}
//set search editText text empty
etSearchPeople.setText("");
//hide soft keyboard
hideSoftKeyBoard();
//remove focus
etSearchPeople.clearFocus();
}
/**
* Hide soft keyboard
*/
private void hideSoftKeyBoard() {
// Check if no view has focus:
View view = this.getCurrentFocus();
if (view != null) {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
}
private void onTextChangeOfStreamSearchEditText() {
etSearchStream.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
try {
streamsDrawerAdapter.changeCursor(getSteamCursorGenerator().call());
} catch (Exception e) {
ZLog.logException(e);
}
}
@Override
public void afterTextChanged(Editable s) {
}
});
}
private Callable<Cursor> getSteamCursorGenerator() {
Callable<Cursor> steamCursorGenerator = new Callable<Cursor>() {
@Override
public Cursor call() throws Exception {
String query = "SELECT s.id as _id, s.name, s.color"
+ " FROM streams as s LEFT JOIN messages as m ON s.id=m.stream ";
String selectArg = null;
if (!etSearchStream.getText().toString().equals("") && !etSearchStream.getText().toString().isEmpty()) {
//append where clause
selectArg = etSearchStream.getText().toString() + '%';
query += " WHERE s.name LIKE ? " + " and s." + Stream.SUBSCRIBED_FIELD + " = " + "1 ";
//set visibility of this image false
ivSearchStreamCancel.setVisibility(View.VISIBLE);
} else {
//set visibility of this image false
query += " WHERE s." + Stream.SUBSCRIBED_FIELD + " = " + "1 ";
ivSearchStreamCancel.setVisibility(View.GONE);
}
//append group by
query += " group by s.name order by s.name COLLATE NOCASE";
if (selectArg != null) {
return ((AndroidDatabaseResults) app.getDao(Stream.class).queryRaw(query, selectArg).closeableIterator().getRawResults()).getRawCursor();
} else {
return ((AndroidDatabaseResults) app.getDao(Stream.class).queryRaw(query).closeableIterator().getRawResults()).getRawCursor();
}
}
};
return steamCursorGenerator;
}
private void setUpPeopleList() {
try {
this.peopleAdapter = new RefreshableCursorAdapter(
this.getApplicationContext(), R.layout.stream_tile,
getPeopleCursorGenerator().call(), getPeopleCursorGenerator(), new String[]{
Person.NAME_FIELD, Person.EMAIL_FIELD}, new int[]{
R.id.name, R.id.stream_dot}, 0);
peopleAdapter.setViewBinder(peopleBinder);
peopleDrawer.setAdapter(peopleAdapter);
} catch (SQLException e) {
ZLog.logException(e);
throw new RuntimeException(e);
} catch (Exception e) {
ZLog.logException(e);
}
}
private void onTextChangeOfPeopleSearchEditText() {
etSearchPeople.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
try {
peopleAdapter.changeCursor(getPeopleCursorGenerator().call());
} catch (Exception e) {
ZLog.logException(e);
}
}
@Override
public void afterTextChanged(Editable s) {
}
});
}
private Callable<Cursor> getPeopleCursorGenerator() {
Callable<Cursor> peopleGenerator = new Callable<Cursor>() {
@Override
public Cursor call() throws Exception {
// TODO Auto-generated method stub
List<Person> people;
if (etSearchPeople.getText().toString().equals("") || etSearchPeople.getText().toString().isEmpty()) {
people = app.getDao(Person.class).queryBuilder()
.where().eq(Person.ISBOT_FIELD, false).and()
.eq(Person.ISACTIVE_FIELD, true).query();
//set visibility of this image false
ivSearchPeopleCancel.setVisibility(View.GONE);
} else {
people = app.getDao(Person.class).queryBuilder()
.where().eq(Person.ISBOT_FIELD, false).and()
.like(Person.NAME_FIELD, "%" + etSearchPeople.getText().toString() + "%").and()
.eq(Person.ISACTIVE_FIELD, true).query();
//set visibility of this image false
ivSearchPeopleCancel.setVisibility(View.VISIBLE);
}
Person.sortByPresence(app, people);
String[] columnsWithPresence = new String[]{"_id",
Person.EMAIL_FIELD, Person.NAME_FIELD};
MatrixCursor sortedPeopleCursor = new MatrixCursor(
columnsWithPresence);
for (Person person : people) {
Object[] row = new Object[]{person.getId(), person.getEmail(),
person.getName()};
sortedPeopleCursor.addRow(row);
}
// add private messages row
MatrixCursor allPrivateMessages = new MatrixCursor(
sortedPeopleCursor.getColumnNames());
Object[] row = new Object[]{allPeopleId, "",
"All private messages"};
allPrivateMessages.addRow(row);
return new MergeCursor(new Cursor[]{
allPrivateMessages, sortedPeopleCursor});
}
};
return peopleGenerator;
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (mNotificationManager == null) {
mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
}
if (mBuilder == null) {
mBuilder = (NotificationCompat.Builder) new NotificationCompat.Builder(this)
.setContentTitle(getString(R.string.notif_title))
.setColor(ContextCompat.getColor(this, R.color.notif_background));
}
// Get action and MIME type of intent
String action = intent.getAction();
String type = intent.getType();
if (Intent.ACTION_SEND.equals(action) && type != null) {
if ("text/plain".equals(type)) {
// Handle text being sent
handleSentText(intent);
} else {
// Handle single file being sent
handleSentFile((Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM));
}
}
// extract file path of edited image
String filePath = intent.getStringExtra(Intent.EXTRA_TEXT);
if (action == null) {
if (!TextUtils.isEmpty(filePath)) {
// start upload of photo
File photoFile = new File(filePath);
uploadFile(photoFile);
} else {
// photo was deleted and camera is launched again to capture a new photo
dispatchTakePictureIntent();
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_TAKE_PHOTO && resultCode == RESULT_OK) {
// send file path to PhotoSendActivity
Intent photoSendIntent = new Intent(this, PhotoSendActivity.class);
photoSendIntent.putExtra(Intent.EXTRA_TEXT, mCurrentPhotoPath);
startActivity(photoSendIntent);
// activity transition animation
ActivityTransitionAnim.transition(ZulipActivity.this);
} else if (requestCode == REQUEST_PICK_FILE && resultCode == RESULT_OK) {
List<Uri> fileUris = new ArrayList<>();
if (data.getData() != null) {
fileUris.add(data.getData());
}
// intent.getClipData was added in api 16
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
ClipData clipData = data.getClipData();
if (clipData != null) {
for (int i = 0; i < clipData.getItemCount(); i++) {
fileUris.add(clipData.getItemAt(i).getUri());
}
}
}
for (Uri file : fileUris) {
handleSentFile(file);
}
}
}
/**
* Function invoked when a user shares a text with the zulip app
*
* @param intent passed to the activity with action SEND
*/
private void handleSentText(Intent intent) {
String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
if (sharedText != null) {
// Update UI to reflect text being shared
displayFAB(false);
displayChatBox(true);
messageEt.append(" " + sharedText);
}
}
/**
* Function invoked when a user shares an image with the zulip app
*
* @param fileUri obtained from intent passed to the activity
*/
@SuppressLint("InlinedApi")
private void handleSentFile(Uri fileUri) {
mFileUri = fileUri;
if (mFileUri != null) {
// check if user has granted read external storage permission
// for Android 6.0 or higher
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// we need to request the permission.
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
PERMISSION_REQUEST_READ_STORAGE);
} else {
// permission already granted
// start with file upload
startFileUpload();
}
} else {
Toast.makeText(this, R.string.cannot_find_file, Toast.LENGTH_SHORT).show();
}
}
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case PERMISSION_REQUEST_READ_STORAGE: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// permission granted
// start with file upload
startFileUpload();
} else {
// permission denied
Toast.makeText(this, R.string.cannot_upload_file, Toast.LENGTH_SHORT).show();
}
}
break;
}
}
/**
* This function is called when camera icon is clicked. It send out a
* MediaStore.ACTION_IMAGE_CAPTURE action intent {@link Intent}.
*/
private void dispatchTakePictureIntent() {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// Ensure that there's a camera activity to handle the intent
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
// Create the File where the photo should go
File photoFile = null;
try {
photoFile = createPhotoFile();
} catch (IOException ex) {
// Error occurred while creating the File
ZLog.logException(ex);
}
// Continue only if the File was successfully created
if (photoFile != null) {
mFileUri = FileProvider.getUriForFile(this,
"com.zulip.fileprovider",
photoFile);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, mFileUri);
// grant uri permissions for lower api levels
List<ResolveInfo> resInfoList = this.getPackageManager().queryIntentActivities(takePictureIntent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
this.grantUriPermission(packageName, mFileUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
// activity transition animation
ActivityTransitionAnim.transition(ZulipActivity.this);
}
}
}
/**
* This function creates a file for the photo to be captured
*
* @return new {@link File} object where photo will be stored
* @throws IOException
*/
private File createPhotoFile() throws IOException {
// Create an image file name using timestamp
String timeStamp = SimpleDateFormat.getDateTimeInstance().format(new java.util.Date());
String imageFileName = "JPEG_" + timeStamp + "_";
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File image = File.createTempFile(
imageFileName, /* prefix */
".jpg", /* suffix */
storageDir /* directory */
);
// Save image path to send to PhotoSendActivity
mCurrentPhotoPath = image.getAbsolutePath();
return image;
}
/**
* Helper function to update UI to indicate file is being uploaded and call
* {@link #uploadFile(File)} to upload the image.
*/
private void startFileUpload() {
File file = null;
if (FileUtils.isLegacy(mFileUri)) {
file = FileUtils.getTempFileFromContentUri(this, mFileUri);
} else {
// get actual file path
String filePath = FileUtils.getPath(this, mFileUri);
if (filePath != null) {
file = new File(filePath);
} else if ("content".equalsIgnoreCase(mFileUri.getScheme())) {
file = FileUtils.getTempFileFromContentUri(this, mFileUri);
}
}
if (file == null) {
Toast.makeText(this, R.string.invalid_file, Toast.LENGTH_SHORT).show();
return;
}
// upload the file asynchronously to the server
uploadFile(file);
}
@NonNull
private MultipartBody.Part prepareFilePart(String partName, final File file, int notificationId, UploadProgressRequest.UploadCallbacks callbacks) {
// create UploadProgressRequest instance from file
UploadProgressRequest request = new UploadProgressRequest(file, callbacks, notificationId);
// MultipartBody.Part is used to send also the actual file name
return MultipartBody.Part.createFormData(partName, file.getName(), request);
}
public void cancelRequest(int notificationId) throws NullPointerException {
Call<UploadResponse> call = mCancelHashMap.get(notificationId);
if (call != null) {
call.cancel();
mCancelHashMap.remove(notificationId);
}
}
/**
* Modifies notification {@link Notifications} of specifies {@param notificationId}
* and sets its {@param content}.
*
* @param notificationId
* @param content
*/
private void setNotification(int notificationId, String content) {
mBuilder.setSmallIcon(android.R.drawable.stat_sys_upload)
.setContentTitle(getString(R.string.notif_title))
.setContentText(content)
.setAutoCancel(false)
.setOngoing(false)
// Removes the progress bar
.setProgress(0, 0, false);
mBuilder.mActions.clear();
PendingIntent contentIntent = PendingIntent.getActivity(
getApplicationContext(),
0,
new Intent(),
PendingIntent.FLAG_UPDATE_CURRENT);
mBuilder.setContentIntent(contentIntent);
mNotificationManager.notify(notificationId, mBuilder.build());
}
/**
* Updates upload notification with passed percentage {@param percentage} and progress string
* {@param progress} as content.
*
* @param notificationId
* @param percentage
*/
private void progressNotification(int notificationId, int percentage, String progress, String title, PendingIntent pendingIntent) {
mBuilder.setSmallIcon(android.R.drawable.stat_sys_upload)
.setContentTitle(title)
.setContentText(progress)
.setAutoCancel(false)
.setOngoing(true)
.setProgress(100, percentage, false);
mBuilder.mActions.clear();
mBuilder.addAction(R.drawable.ic_cancel_black_24dp, getString(R.string.cancel_content_desp), pendingIntent);
mNotificationManager.notify(notificationId, mBuilder.build());
}
/**
* Shows success notification.
*
* @param notificationId
* @param content
*/
private void endNotification(int notificationId, String content) {
mBuilder.setSmallIcon(R.drawable.ic_done_white_24dp)
.setContentTitle(getString(R.string.notif_title))
.setContentText(content)
.setAutoCancel(true)
.setOngoing(false)
// Removes the progress bar
.setProgress(0, 0, false);
PendingIntent contentIntent = PendingIntent.getActivity(
getApplicationContext(),
0,
new Intent(),
PendingIntent.FLAG_UPDATE_CURRENT);
mBuilder.setContentIntent(contentIntent);
mBuilder.mActions.clear();
mNotificationManager.notify(notificationId, mBuilder.build());
}
/**
* Function to upload file asynchronously to the server using retrofit callback
* upload {@link com.zulip.android.service.ZulipServices#upload(MultipartBody.Part)}
*
* @param file on local storage
*/
private void uploadFile(final File file) {
// check if file size is greater than 10MB
if (file.length() / Math.pow(1024, 2) > 10) {
Toast.makeText(this, R.string.upload_big_file, Toast.LENGTH_SHORT).show();
return;
}
// generate unique notification Id for this upload
final int notifId = (int) (new Date().getTime() % Integer.MAX_VALUE);
// cancel pending intent
Intent actionIntent = new Intent(this, GcmBroadcastReceiver.class);
actionIntent.putExtra("id", notifId);
actionIntent.setAction(Constants.CANCEL);
final PendingIntent piAction = PendingIntent.getBroadcast(this, notifId, actionIntent, PendingIntent.FLAG_UPDATE_CURRENT);
// update notification after every one second
final long startTime = System.currentTimeMillis();
final int[] counter = {0};
UploadProgressRequest.UploadCallbacks progressListener = new UploadProgressRequest.UploadCallbacks() {
@Override
public void onProgressUpdate(int percentage, String progress, int notificationId) {
if (System.currentTimeMillis() - startTime >= 1000 * counter[0]) {
// update notification
progressNotification(notificationId, percentage, progress, file.getName(), piAction);
counter[0]++;
}
}
};
// MultipartBody.Part is used to send also the actual file name
MultipartBody.Part body = prepareFilePart("file", file, notifId, progressListener);
// finally, execute the request
// create upload service client
Call<UploadResponse> call = ((ZulipApp) getApplicationContext()).getUploadServices().upload(body);
if (mCancelHashMap == null) {
mCancelHashMap = new HashMap<>();
}
mCancelHashMap.put(notifId, call);
Toast.makeText(this, R.string.upload_started_str, Toast.LENGTH_SHORT).show();
// start notification
setNotification(notifId, getString(R.string.init_notif_title) + getString(R.string.to_string) + getNotifTitle());
call.enqueue(new DefaultCallback<UploadResponse>() {
@Override
public void onSuccess(Call<UploadResponse> call, Response<UploadResponse> response) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && isDestroyed()) {
return;
}
String filePathOnServer = "";
UploadResponse uploadResponse = response.body();
filePathOnServer = uploadResponse.getUri();
if (!filePathOnServer.equals("")) {
endNotification(notifId, getString(R.string.finish_notif_title));
// add uploaded file url on server to composed message
messageEt.append("\n[" + file.getName() + "](" +
UrlHelper.addHost(filePathOnServer) + ")");
displayFAB(false);
displayChatBox(true);
} else {
endNotification(notifId, getString(R.string.failed_to_upload));
}
if (!(mCancelHashMap != null && mCancelHashMap.size() == 1)) {
mNotificationManager.cancel(notifId);
}
if (mCancelHashMap != null) {
mCancelHashMap.remove(notifId);
mCancelHashMap = mCancelHashMap.isEmpty() ? null : mCancelHashMap;
}
}
@Override
public void onError(Call<UploadResponse> call, Response<UploadResponse> response) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && isDestroyed()) {
return;
}
endNotification(notifId, getString(R.string.failed_to_upload));
mNotificationManager.cancel(notifId);
}
@Override
public void onFailure(Call<UploadResponse> call, Throwable t) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && isDestroyed()) {
return;
}
if (call.isCanceled()) {
mNotificationManager.cancel(notifId);
return;
}
endNotification(notifId, getString(R.string.failed_to_upload));
mNotificationManager.cancel(notifId);
ZLog.logException(t);
}
});
}
private String getNotifTitle() {
String title = "";
MessageType messageType = isCurrentModeStream() ? MessageType.STREAM_MESSAGE : MessageType.PRIVATE_MESSAGE;
if (messageType == MessageType.STREAM_MESSAGE) {
title += streamActv.getText().toString();
title += " > " + topicActv.getText().toString();
}
if (TextUtils.isEmpty(title) || title.equals(" > ")) {
NarrowFilter filter = getCurrentMessageList().filter;
if (filter == null) {
title = getString(R.string.app_name);
} else {
title = filter.getTitle() + (filter.getSubtitle() != null ? " > " + filter.getSubtitle() : "");
}
}
return title;
}
/**
* Returns a cursor for the combinedAdapter used to suggest Emoji when ':' is typed in the {@link #messageEt}
*
* @param emoji A string to search in the existing database
*/
private Cursor makeEmojiCursor(CharSequence emoji)
throws SQLException {
if (emoji == null) {
emoji = "";
}
return ((AndroidDatabaseResults) app
.getDao(Emoji.class)
.queryRaw("SELECT rowid _id,name FROM emoji WHERE name LIKE '%" + emoji + "%'")
.closeableIterator().getRawResults()).getRawCursor();
}
private Cursor makePeopleNameCursor(CharSequence name) throws SQLException {
if (name == null) {
name = "";
}
return ((AndroidDatabaseResults) app
.getDao(Person.class)
.queryRaw(
"SELECT rowid _id, * FROM people WHERE "
+ Person.ISBOT_FIELD + " = 0 AND "
+ Person.ISACTIVE_FIELD + " = 1 AND "
+ Person.NAME_FIELD
+ " LIKE ? ESCAPE '\\' ORDER BY "
+ Person.NAME_FIELD + " COLLATE NOCASE",
DatabaseHelper.likeEscape(name.toString()) + "%")
.closeableIterator().getRawResults()).getRawCursor();
}
private void setupFab() {
fab = (FloatingActionButton) findViewById(R.id.fab);
chatBox = (SwipeRemoveLinearLayout) findViewById(R.id.messageBoxContainer);
chatBox.registerToSwipeEvents(this);
addFileBtn.setColorFilter(getResources().getColorStateList(R.color.colorTextSecondary).getColorForState(addFileBtn.getDrawableState(), 0));
sendBtn.setColorFilter(getResources().getColorStateList(R.color.colorTextSecondary).getColorForState(sendBtn.getDrawableState(), 0));
togglePrivateStreamBtn.setColorFilter(getResources().getColorStateList(R.color.colorTextSecondary).getColorForState(togglePrivateStreamBtn.getDrawableState(), 0));
fabHidder = new CountDownTimer(Constants.HIDE_FAB_AFTER_SEC * 1000, Constants.HIDE_FAB_AFTER_SEC * 1000) {
public void onTick(long millisUntilFinished) {
}
public void onFinish() {
if (!isTextFieldFocused) {
displayFAB(true);
displayChatBox(false);
} else {
start();
}
}
};
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
currentList.stopRecyclerViewScroll();
displayChatBox(true);
displayFAB(false);
fabHidder.start();
showSoftKeyboard();
checkForChatBoxFocusRequest();
}
});
}
/**
* Shows soft keyboard
*/
private void showSoftKeyboard() {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, InputMethodManager.HIDE_IMPLICIT_ONLY);
}
private void displayChatBox(boolean show) {
if (show) {
showView(chatBox);
} else {
hideView(chatBox);
hideSoftKeyBoard();
}
}
private void displayFAB(boolean show) {
if (show) {
showView(fab);
} else {
hideView(fab);
}
}
@SuppressLint("NewApi")
public void hideView(final View view) {
ViewPropertyAnimator animator = view.animate()
.translationY((view instanceof AppBarLayout) ? -1 * view.getHeight() : view.getHeight())
.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR)
.setDuration(200);
animator.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
view.setVisibility(View.GONE);
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
animator.start();
}
@SuppressLint("NewApi")
public void showView(final View view) {
ViewPropertyAnimator animator = view.animate()
.translationY(0)
.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR)
.setDuration(200);
animator.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
view.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationEnd(Animator animator) {
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
animator.start();
}
/**
* Setup the streams Drawer which has a {@link ExpandableListView} categorizes the stream and subject
*/
private void setupListViewAdapter() {
streamsDrawerAdapter = null;
String[] groupFrom = {Stream.NAME_FIELD, Stream.COLOR_FIELD};
int[] groupTo = {R.id.name, R.id.stream_dot};
// Comparison of data elements and View
String[] childFrom = {Message.SUBJECT_FIELD};
int[] childTo = {R.id.name_child};
final ExpandableListView streamsDrawer = (ExpandableListView) findViewById(R.id.streams_drawer);
streamsDrawer.setGroupIndicator(null);
try {
streamsDrawerAdapter = new ExpandableStreamDrawerAdapter(this, getSteamCursorGenerator().call(),
R.layout.stream_tile_new, groupFrom,
groupTo, R.layout.stream_tile_child, childFrom,
childTo);
} catch (Exception e) {
ZLog.logException(e);
}
streamsDrawer.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
@Override
public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) {
switch (v.getId()) {
case R.id.name_child_container:
String streamName = ((Cursor) streamsDrawer.getExpandableListAdapter().getGroup(groupPosition)).getString(1);
String subjectName = ((Cursor) streamsDrawer.getExpandableListAdapter().getChild(groupPosition, childPosition)).getString(0);
onNarrow(new NarrowFilterStream(streamName, subjectName));
onNarrowFillSendBoxStream(streamName, subjectName, false);
break;
default:
return false;
}
return false;
}
});
streamsDrawer.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
int previousClick = -1;
@Override
public boolean onGroupClick(ExpandableListView expandableListView, View view, int position, long l) {
resetStreamSearch();
String streamName = ((TextView) view.findViewById(R.id.name)).getText().toString();
doNarrowToLastRead(streamName);
if (!isTablet())
drawerLayout.openDrawer(GravityCompat.START);
if (previousClick != -1 && expandableListView.getCount() > previousClick) {
expandableListView.collapseGroup(previousClick);
}
expandableListView.expandGroup(position);
previousClick = position;
onNarrowFillSendBoxStream(streamName, "", false);
return true;
}
});
streamsDrawerAdapter.setViewBinder(new SimpleCursorTreeAdapter.ViewBinder() {
@Override
public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
switch (view.getId()) {
case R.id.name:
TextView name = (TextView) view;
final String streamName = cursor.getString(columnIndex);
name.setText(streamName);
//Change color in the drawer if this stream is inHomeView only.
if (!Stream.getByName(app, streamName).getInHomeView()) {
name.setTextColor(ContextCompat.getColor(ZulipActivity.this, R.color.colorTextTertiary));
} else {
name.setTextColor(ContextCompat.getColor(ZulipActivity.this, R.color.colorTextPrimary));
}
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
resetStreamSearch();
doNarrowToLastRead(streamName);
onNarrowFillSendBoxStream(streamName, "", false);
}
});
return true;
case R.id.stream_dot:
// Set the color of the (currently white) dot
view.setVisibility(View.VISIBLE);
view.getBackground().setColorFilter(cursor.getInt(columnIndex),
PorterDuff.Mode.MULTIPLY);
return true;
case R.id.unread_group:
TextView unreadGroupTextView = (TextView) view;
final String unreadGroupCount = cursor.getString(columnIndex);
// if (unreadGroupCount.equals("0")) {
// unreadGroupTextView.setVisibility(View.GONE);
// } else {
// unreadGroupTextView.setText(unreadGroupCount);
// unreadGroupTextView.setVisibility(View.VISIBLE);
// }
return true;
case R.id.unread_child:
TextView unreadChildTextView = (TextView) view;
final String unreadChildNumber = cursor.getString(columnIndex);
if (unreadChildNumber.equals("0")) {
unreadChildTextView.setVisibility(View.GONE);
} else {
unreadChildTextView.setText(unreadChildNumber);
unreadChildTextView.setVisibility(View.VISIBLE);
}
return true;
case R.id.name_child:
TextView name_child = (TextView) view;
name_child.setText(cursor.getString(columnIndex));
if (mMutedTopics.isTopicMute(cursor.getInt(1), cursor.getString(columnIndex))) {
name_child.setTextColor(ContextCompat.getColor(ZulipActivity.this, R.color.colorTextSecondary));
}
return true;
}
return false;
}
});
streamsDrawer.setAdapter(streamsDrawerAdapter);
}
/**
* Helper function used to call {@link ZulipActivity#onNarrow(NarrowFilter, int)} or
* {@link ZulipActivity#onNarrow(NarrowFilter)} based on last message read in {@param streamName} stream.
*
* @param streamName stream name
*/
public void doNarrowToLastRead(String streamName) {
// get last message read in stream
Message message = Stream.getLastMessageRead(app, streamName);
if (message != null) {
// fetch results around the last message read
onNarrow(new NarrowFilterStream(streamName, null), message.getId());
} else {
// new stream
onNarrow(new NarrowFilterStream(streamName, null));
}
}
/**
* Change streamsDrawerAdapter cursor to default
* Clear text of etSearchStream
* Remove focus of etSearchStream
*/
private void resetStreamSearch() {
try {
streamsDrawerAdapter.changeCursor(getSteamCursorGenerator().call());
} catch (Exception e) {
ZLog.logException(e);
}
etSearchStream.setText("");
//hide soft keyboard
hideSoftKeyBoard();
//remove focus
etSearchStream.clearFocus();
}
/**
* Initiates the streams Drawer if the streams in the drawer is 0.
*/
public void checkAndSetupStreamsDrawer() {
setupListViewAdapter();
}
private void sendMessage() {
if (isCurrentModeStream()) {
if (TextUtils.isEmpty(streamActv.getText().toString())) {
streamActv.setError(getString(R.string.stream_error));
streamActv.requestFocus();
return;
} else {
Stream stream = Stream.streamCheckBeforeMessageSend(app, streamActv.getText().toString());
//check whether stream exists or not
if (stream == null) {
streamActv.setError(getString(R.string.stream_not_exists));
streamActv.requestFocus();
return;
}// check whether user is subscribed or not
else if (!stream.isSubscribed()) {
Toast.makeText(ZulipActivity.this, getString(R.string.message_not_subscribed)
+ " " + streamActv.getText().toString() + " " + getString(R.string.keyword_stream), Toast.LENGTH_SHORT).show();
return;
}
}
if (TextUtils.isEmpty(topicActv.getText().toString())) {
topicActv.setError(getString(R.string.subject_error));
topicActv.requestFocus();
return;
}
} else {
if (TextUtils.isEmpty(topicActv.getText().toString())) {
topicActv.setError(getString(R.string.person_error));
topicActv.requestFocus();
return;
}
}
if (TextUtils.isEmpty(messageEt.getText().toString())) {
messageEt.setError(getString(R.string.no_message_error));
messageEt.requestFocus();
return;
}
final String sendingMsg = getResources().getString(R.string.sending_message);
sendingMessage(true, sendingMsg);
MessageType messageType = isCurrentModeStream() ? MessageType.STREAM_MESSAGE : MessageType.PRIVATE_MESSAGE;
Message msg = new Message(app);
msg.setSender(app.getYou());
if (messageType == MessageType.STREAM_MESSAGE) {
msg.setType(messageType);
msg.setStream(new Stream(streamActv.getText().toString()));
msg.setSubject(topicActv.getText().toString());
} else if (messageType == MessageType.PRIVATE_MESSAGE) {
msg.setType(messageType);
msg.setRecipient(topicActv.getText().toString().split(","));
}
msg.setContent(messageEt.getText().toString());
AsyncSend sender = new AsyncSend(that, msg);
sender.setCallback(new ZulipAsyncPushTask.AsyncTaskCompleteListener() {
public void onTaskComplete(String result, JSONObject jsonObject) {
Toast.makeText(ZulipActivity.this, R.string.message_sent, Toast.LENGTH_SHORT).show();
messageEt.setText("");
sendingMessage(false, sendingMsg);
}
public void onTaskFailure(String result) {
Log.d("onTaskFailure", "Result: " + result);
Toast.makeText(ZulipActivity.this, R.string.message_error, Toast.LENGTH_SHORT).show();
sendingMessage(false, sendingMsg);
}
});
sender.execute();
}
/**
* Disable chatBox and show a loading footer while sending the message.
*/
private void sendingMessage(boolean isSending, String message) {
streamActv.setEnabled(!isSending);
textView.setEnabled(!isSending);
messageEt.setEnabled(!isSending);
topicActv.setEnabled(!isSending);
sendBtn.setEnabled(!isSending);
addFileBtn.setEnabled(!isSending);
togglePrivateStreamBtn.setEnabled(!isSending);
if (isSending) {
TextView msg = (TextView) composeStatus.findViewById(R.id.sending_message);
msg.setText(message);
composeStatus.setVisibility(View.VISIBLE);
} else
composeStatus.setVisibility(View.GONE);
}
/**
* Setup adapter's for the {@link AutoCompleteTextView}
* <p>
* These adapters are being intialized -
* <p>
* {@link #streamActvAdapter} Adapter for suggesting all the stream names in this AutoCompleteTextView
* {@link #emailActvAdapter} Adapter for suggesting all the person email's in this AutoCompleteTextView
* {@link #subjectActvAdapter} Adapter for suggesting all the topic for the stream specified in the {@link #streamActv} in this AutoCompleteTextView
*/
private void setUpAdapter() {
streamActvAdapter = new SimpleCursorAdapter(
that, R.layout.stream_tile, null,
new String[]{Stream.NAME_FIELD},
new int[]{R.id.name}, 0);
streamActvAdapter.setCursorToStringConverter(new SimpleCursorAdapter.CursorToStringConverter() {
@Override
public CharSequence convertToString(Cursor cursor) {
int index = cursor.getColumnIndex(Stream.NAME_FIELD);
return cursor.getString(index);
}
});
streamActvAdapter.setFilterQueryProvider(new FilterQueryProvider() {
@Override
public Cursor runQuery(CharSequence charSequence) {
try {
return makeStreamCursor(charSequence);
} catch (SQLException e) {
ZLog.logException(e);
Log.e("SQLException", "SQL not correct", e);
return null;
}
}
});
subjectActvAdapter = new SimpleCursorAdapter(
that, R.layout.stream_tile, null,
new String[]{Message.SUBJECT_FIELD},
new int[]{R.id.name}, 0);
subjectActvAdapter.setCursorToStringConverter(new SimpleCursorAdapter.CursorToStringConverter() {
@Override
public CharSequence convertToString(Cursor cursor) {
int index = cursor.getColumnIndex(Message.SUBJECT_FIELD);
return cursor.getString(index);
}
});
subjectActvAdapter.setFilterQueryProvider(new FilterQueryProvider() {
@Override
public Cursor runQuery(CharSequence charSequence) {
try {
return makeSubjectCursor(streamActv.getText().toString(), charSequence);
} catch (SQLException e) {
ZLog.logException(e);
Log.e("SQLException", "SQL not correct", e);
return null;
}
}
});
emailActvAdapter = new SimpleCursorAdapter(
that, R.layout.stream_tile, null,
new String[]{Person.EMAIL_FIELD},
new int[]{R.id.name}, 0);
emailActvAdapter
.setCursorToStringConverter(new SimpleCursorAdapter.CursorToStringConverter() {
@Override
public CharSequence convertToString(Cursor cursor) {
String text = topicActv.getText().toString();
String prefix;
int lastIndex = text.lastIndexOf(',');
if (lastIndex != -1) {
prefix = text.substring(0, lastIndex + 1);
} else {
prefix = "";
}
int index = cursor.getColumnIndex(Person.EMAIL_FIELD);
return prefix + cursor.getString(index);
}
});
emailActvAdapter.setFilterQueryProvider(new FilterQueryProvider() {
@Override
public Cursor runQuery(CharSequence charSequence) {
try {
return makePeopleCursor(charSequence);
} catch (SQLException e) {
ZLog.logException(e);
Log.e("SQLException", "SQL not correct", e);
return null;
}
}
});
sendingMessage(false, getResources().getString(R.string.sending_message));
}
/**
* Creates a cursor to get the streams saved in the database
*
* @param streamName Filter out streams name containing this string
*/
private Cursor makeStreamCursor(CharSequence streamName)
throws SQLException {
if (streamName == null) {
streamName = "";
}
return ((AndroidDatabaseResults) app
.getDao(Stream.class)
.queryRaw(
"SELECT rowid _id, * FROM streams WHERE "
+ Stream.SUBSCRIBED_FIELD + " = 1 AND "
+ Stream.NAME_FIELD
+ " LIKE ? ESCAPE '\\' ORDER BY "
+ Stream.NAME_FIELD + " COLLATE NOCASE",
DatabaseHelper.likeEscape(streamName.toString()) + "%")
.closeableIterator().getRawResults()).getRawCursor();
}
/**
* Creates a cursor to get the topics in the stream in
*
* @param stream from which topics similar to {@param subject} are selected
* @param subject Filter out subject containing this string
*/
private Cursor makeSubjectCursor(CharSequence stream, CharSequence subject)
throws SQLException {
if (subject == null) {
subject = "";
}
if (stream == null) {
stream = "";
}
AndroidDatabaseResults results = (AndroidDatabaseResults) app
.getDao(Message.class)
.queryRaw(
"SELECT DISTINCT "
+ Message.SUBJECT_FIELD
+ ", 1 AS _id FROM messages JOIN streams ON streams."
+ Stream.ID_FIELD + " = messages."
+ Message.STREAM_FIELD + " WHERE "
+ Message.SUBJECT_FIELD
+ " LIKE ? ESCAPE '\\' AND "
+ Stream.NAME_FIELD + " = ? ORDER BY "
+ Message.SUBJECT_FIELD + " COLLATE NOCASE",
DatabaseHelper.likeEscape(subject.toString()) + "%",
stream.toString()).closeableIterator().getRawResults();
return results.getRawCursor();
}
/**
* Creates a cursor to get the E-Mails stored in the database
*
* @param email Filter out emails containing this string
*/
private Cursor makePeopleCursor(CharSequence email) throws SQLException {
if (email == null) {
email = "";
}
String[] pieces = TextUtils.split(email.toString(), ",");
String piece;
if (pieces.length == 0) {
piece = "";
} else {
piece = pieces[pieces.length - 1].trim();
}
return ((AndroidDatabaseResults) app
.getDao(Person.class)
.queryRaw(
"SELECT rowid _id, * FROM people WHERE "
+ Person.ISBOT_FIELD + " = 0 AND "
+ Person.ISACTIVE_FIELD + " = 1 AND "
+ Person.EMAIL_FIELD
+ " LIKE ? ESCAPE '\\' ORDER BY "
+ Person.NAME_FIELD + " COLLATE NOCASE",
DatabaseHelper.likeEscape(piece) + "%")
.closeableIterator().getRawResults()).getRawCursor();
}
private void switchToStream() {
removeEditTextErrors();
if (!isCurrentModeStream()) {
switchView();
}
}
private void switchToPrivate() {
removeEditTextErrors();
if (isCurrentModeStream()) {
switchView();
}
}
private boolean isCurrentModeStream() {
//The TextView is VISIBLE which means currently send to stream is on.
return textView.getVisibility() == View.VISIBLE;
}
private void removeEditTextErrors() {
streamActv.setError(null);
topicActv.setError(null);
messageEt.setError(null);
}
/**
* Switch from Private to Stream or vice versa in chatBox
*/
private void switchView() {
if (isCurrentModeStream()) { //Person
togglePrivateStreamBtn.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_action_bullhorn));
tempStreamSave = topicActv.getText().toString();
topicActv.setText(null);
topicActv.setHint(R.string.hint_person);
topicActv.setAdapter(emailActvAdapter);
streamActv.setVisibility(View.GONE);
textView.setVisibility(View.GONE);
} else { //Stream
topicActv.setText(tempStreamSave);
togglePrivateStreamBtn.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_action_person));
streamActv.setEnabled(true);
topicActv.setHint(R.string.hint_subject);
streamActv.setHint(R.string.hint_stream);
streamActv.setVisibility(View.VISIBLE);
textView.setVisibility(View.VISIBLE);
topicActv.setVisibility(View.VISIBLE);
streamActv.setAdapter(streamActvAdapter);
topicActv.setAdapter(subjectActvAdapter);
}
}
@Override
public void clearChatBox() {
if (messageEt != null) {
if (TextUtils.isEmpty(messageEt.getText())) {
topicActv.setText("");
streamActv.setText("");
}
}
}
@Override
public void setLayoutBehaviour(LinearLayoutManager linearLayoutManager, RecyclerMessageAdapter adapter) {
CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
layoutParams.setBehavior(new RemoveViewsOnScroll(linearLayoutManager, adapter));
appBarLayout.requestLayout();
layoutParams = (CoordinatorLayout.LayoutParams) fab.getLayoutParams();
layoutParams.setBehavior(new RemoveViewsOnScroll(linearLayoutManager, adapter));
fab.setLayoutParams(layoutParams);
topSnackBar.setMessagesLayoutManager(linearLayoutManager);
topSnackBar.setMessagesAdapter(adapter);
}
/**
* called when action bar hides
* due to down scroll in message list
*/
public void onHideActionBar() {
isActionBarShown = false;
try {
CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) viewHolder.itemView.getLayoutParams();
layoutParams.setMargins(0, 0, 0, 0);
viewHolder.itemView.requestLayout();
} catch (NullPointerException ignored) {
}
}
/**
* called when action bar is shown
* due to up scroll in message list
*/
public void onShowActionBar() {
isActionBarShown = true;
try {
CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) viewHolder.itemView.getLayoutParams();
layoutParams.setMargins(0, mToolbarHeightInPx, 0, 0);
viewHolder.itemView.requestLayout();
} catch (NullPointerException ignored) {
}
}
@Override
public void onBackPressed() {
//check for drawer
if (drawerLayout.isDrawerOpen(GravityCompat.START) || drawerLayout.isDrawerOpen(GravityCompat.END)) {
drawerLayout.closeDrawers();
return;
}
//check for narrowed or not
if (narrowedList == null) {
if (backPressedOnce) {
dismissToast();
finish();
return;
}
//Clears search if already open
if (!searchView.isIconified()) {
if (actionBar != null)
actionBar.setHomeAsUpIndicator(R.drawable.ic_menu_24dp);
clearSearch();
return;
}
backPressedOnce = true;
showToast(getString(R.string.press_again_to_exit), Toast.LENGTH_SHORT);
statusUpdateRunnable = new Runnable() {
@Override
public void run() {
backPressedOnce = false;
}
};
statusUpdateHandler.postDelayed(statusUpdateRunnable, 2000);
} else {
narrowedList = null;
getSupportFragmentManager().popBackStack(NARROW,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
clearSearch();
}
}
public void clearSearch() {
//First time clears the searchEditText
searchView.setIconified(true);
//Second time closes the searchEditText
searchView.setIconified(true);
//Setting in search status to false
inSearch = false;
}
/**
* dismiss toast
*/
private void dismissToast() {
if (toast != null) {
toast.cancel();
}
}
/**
* If previous toast is showing cancel it and show new one
*
* @param string message in the toast
* @param duration of the toast to be shown
*/
private void showToast(String string, int duration) {
toast = Toast.makeText(this, string, duration);
if (toast.getView().isShown())
toast.cancel();
toast.show();
}
private void pushListFragment(MessageListFragment list, String back) {
currentList = list;
FragmentTransaction transaction = getSupportFragmentManager()
.beginTransaction();
transaction.replace(R.id.list_fragment_container, list);
if (back != null) {
transaction.addToBackStack(back);
clearSearch();
}
transaction.commit();
getSupportFragmentManager().executePendingTransactions();
}
private void processParams() {
Bundle params = getIntent().getExtras();
if (params == null)
return;
for (String unprocessedParam : params.keySet()) {
Flag param;
if (unprocessedParam.contains(getBaseContext().getPackageName())) {
try {
param = Flag.valueOf(unprocessedParam
.substring(getBaseContext().getPackageName()
.length() + 1));
} catch (IllegalArgumentException e) {
ZLog.logException(e);
Log.e(PARAMS, "Invalid app-specific intent specified.", e);
continue;
}
} else {
continue;
}
switch (param) {
case RESET_DATABASE:
Log.i(PARAMS, "Resetting the database...");
app.resetDatabase();
Log.i(PARAMS, "Database deleted successfully.");
commonProgressDialog.dismiss();
this.finish();
break;
default:
break;
}
}
}
protected void narrow(final Stream stream) {
doNarrow(new NarrowFilterStream(stream, null));
}
private void narrowPMWith(final Person person) {
List<Person> list = new ArrayList<>();
list.add(person);
if (!person.getEmail().equals(app.getYou().getEmail()))
list.add(app.getYou());
doNarrow(new NarrowFilterPM(list));
onNarrowFillSendBoxPrivate(new Person[]{person}, false);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void onListResume(MessageListFragment list) {
currentList = list;
NarrowFilter filter = list.filter;
if (filter == null) {
setupTitleBar(getString(R.string.app_name), null);
this.drawerToggle.setDrawerIndicatorEnabled(true);
} else {
setupTitleBar(filter.getTitle(), filter.getSubtitle());
this.drawerToggle.setDrawerIndicatorEnabled(false);
}
this.drawerLayout.closeDrawers();
}
private void setupTitleBar(String title, String subtitle) {
if (actionBar != null) {
if (title != null) actionBar.setTitle(title);
actionBar.setSubtitle(subtitle);
}
}
/**
* This method creates a new Instance of the MessageListFragment and displays it with the filter.
*/
public void doNarrow(NarrowFilter filter) {
narrowedList = MessageListFragment.newInstance(filter);
// Push to the back stack if we are not already narrowed
pushListFragment(narrowedList, NARROW);
narrowedList.onReadyToDisplay(true);
showView(appBarLayout);
}
/**
* This method creates a new Instance of the MessageListFragment and displays it with the filter
* whiling keeping the view anchored at {@param messageId}.
*
* @param filter NarrowFilter passed
* @param messageId used as anchor for fetching messages
*/
public void doNarrow(NarrowFilter filter, int messageId) {
narrowedList = MessageListFragment.newInstance(filter);
// Push to the back stack if we are not already narrowed
pushListFragment(narrowedList, NARROW);
narrowedList.onReadyToDisplay(true, messageId);
showView(appBarLayout);
}
@Override
public void onNarrowFillSendBoxPrivate(Person peopleList[], boolean openSoftKeyboard) {
displayChatBox(true);
displayFAB(false);
switchToPrivate();
ArrayList<String> names = new ArrayList<String>();
if (peopleList.length == 1) {
names.add(peopleList[0].getEmail());
} else {
for (Person person : peopleList) {
if (person.getId() != app.getYou().getId()) {
names.add(person.getEmail());
}
}
}
topicActv.setText(TextUtils.join(", ", names));
messageEt.requestFocus();
if (openSoftKeyboard) {
showSoftKeyboard();
}
}
/**
* Fills the chatBox according to the {@link MessageType}
*
* @param openSoftKeyboard If true open's up the SoftKeyboard else not.
*/
@Override
public void onNarrowFillSendBox(Message message, boolean openSoftKeyboard) {
displayChatBox(true);
displayFAB(false);
if (message.getType() == MessageType.PRIVATE_MESSAGE) {
switchToPrivate();
topicActv.setText(message.getReplyTo(app));
messageEt.requestFocus();
} else {
switchToStream();
streamActv.setText(message.getStream().getName());
topicActv.setText(message.getSubject());
if ("".equals(message.getSubject())) {
topicActv.requestFocus();
} else messageEt.requestFocus();
}
if (openSoftKeyboard) {
showSoftKeyboard();
}
}
/**
* Fills the chatBox with the stream name and the topic
*
* @param stream Stream name to be filled
* @param subject Subject to be filled
* @param openSoftKeyboard If true open's the softKeyboard else not
*/
public void onNarrowFillSendBoxStream(String stream, String subject, boolean openSoftKeyboard) {
displayChatBox(true);
displayFAB(false);
switchToStream();
streamActv.setText(stream);
topicActv.setText(subject);
if ("".equals(subject)) {
topicActv.requestFocus();
} else messageEt.requestFocus();
if (openSoftKeyboard) {
showSoftKeyboard();
}
}
public void onNarrow(NarrowFilter narrowFilter) {
if (narrowedList == null || !narrowedList.filter.equals(narrowFilter)) {
Log.d("ZulipLog", "inside onNarrow1");
doNarrow(narrowFilter);
}
}
public void onNarrow(NarrowFilter narrowFilter, int messageId) {
if (narrowedList == null || !narrowedList.filter.equals(narrowFilter)) {
doNarrow(narrowFilter, messageId);
}
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// Sync the toggle state after onRestoreInstanceState has occurred.
drawerToggle.syncState();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
Log.d("ASD", "onCreateOptionsMenu: ");
if (this.logged_in) {
getMenuInflater().inflate(R.menu.options, menu);
prepareSearchView(menu);
this.menu = menu;
return true;
}
return false;
}
private boolean prepareSearchView(Menu menu) {
if (this.logged_in && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
// Get the SearchView and set the searchable configuration
final SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
// Assumes current activity is the searchable activity
final MenuItem mSearchMenuItem = menu.findItem(R.id.search);
searchView = (android.support.v7.widget.SearchView) MenuItemCompat.getActionView(mSearchMenuItem);
searchView.setSearchableInfo(searchManager.getSearchableInfo(new ComponentName(getApplicationContext(), ZulipActivity.class)));
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
((EditText) searchView.findViewById(android.support.v7.appcompat.R.id.search_src_text)).setHintTextColor(ContextCompat.getColor(this, R.color.colorTextPrimary));
searchView.setOnQueryTextListener(new android.support.v7.widget.SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String s) {
if (narrowedList != null) {
onNarrow(new NarrowFilterSearch(s, narrowedList.filter));
} else {
onNarrow(new NarrowFilterSearch(s, null));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
mSearchMenuItem.collapseActionView();
}
return true;
}
@Override
public boolean onQueryTextChange(String s) {
return false;
}
});
}
searchView.setOnSearchClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//Change the NavigationDrawer Icon to back on starting search
actionBar.setHomeAsUpIndicator(R.drawable.ic_arrow_back_24dp);
inSearch = true;
}
});
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
//Check if the search is open then close the search
if (inSearch) {
actionBar.setHomeAsUpIndicator(R.drawable.ic_menu_24dp);
clearSearch();
return true;
}
// Pass the event to ActionBarDrawerToggle, if it returns
// true, then it has handled the app icon touch event
if (!isTablet() && drawerToggle.onOptionsItemSelected(item)) {
// Close the right drawer if we opened the left one
drawerLayout.closeDrawer(GravityCompat.END);
return true;
}
// Handle item selection
switch (item.getItemId()) {
case android.R.id.home:
if (isTablet() && narrowedList == null) {
onBackPressed();
}
narrowedList = null;
getSupportFragmentManager().popBackStack(NARROW,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
clearSearch();
break;
case R.id.search:
// show a pop up dialog only if gingerbread or under
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Search Zulip");
final EditText editText = new EditText(this);
builder.setView(editText);
builder.setPositiveButton("Ok",
new DialogInterface.OnClickListener() {
@Override
public void onClick(
DialogInterface dialogInterface, int i) {
String query = editText.getText().toString();
if (narrowedList != null) {
onNarrow(new NarrowFilterSearch(query, narrowedList.filter));
} else {
onNarrow(new NarrowFilterSearch(query, null));
}
}
});
builder.show();
}
break;
case R.id.daynight:
switch (AppCompatDelegate.getDefaultNightMode()) {
case -1:
case AppCompatDelegate.MODE_NIGHT_NO:
setNightMode(AppCompatDelegate.MODE_NIGHT_YES);
break;
case AppCompatDelegate.MODE_NIGHT_YES:
setNightMode(AppCompatDelegate.MODE_NIGHT_NO);
break;
default:
setNightMode(AppCompatDelegate.MODE_NIGHT_NO);
break;
}
break;
case R.id.refresh:
Log.w("menu", "Refreshed manually by user. We shouldn't need this.");
commonProgressDialog.showWithMessage(getString(R.string.refreshing));
onRefresh();
break;
case R.id.today:
//check user selected Today or One Day Before
if (menu != null && menu.getItem(2).getSubMenu().getItem(0).getTitle().equals(getString(R.string.menu_one_day_before))) {
//user selected One Day Before
calendar.add(Calendar.DATE, -1);
onNarrow(new NarrowFilterByDate(calendar.getTime()));
break;
}
//else Narrow to Today
onNarrow(new NarrowFilterByDate());
break;
case R.id.isStarred:
onNarrow(new NarrowFilterStar());
break;
case R.id.enterDate:
//show Dialog with calendar date as selected to pick Date
DatePickerDialog datePickerDialog = new DatePickerDialog(ZulipActivity.this, new DatePickerDialog.OnDateSetListener() {
@Override
public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) {
calendar.set(year, month, dayOfMonth);
onNarrow(new NarrowFilterByDate(calendar.getTime()));
}
}, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH));
//set max date to today so future dates are not selectable
datePickerDialog.getDatePicker().setMaxDate(new Date().getTime());
datePickerDialog.show();
break;
case R.id.logout:
logout();
break;
case R.id.terms:
openUrl(Constants.END_POINT_TERMS_OF_SERVICE);
break;
case R.id.privacy:
openUrl(Constants.END_POINT_PRIVACY);
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
/**
* Open's url in custom tabs if API >= 15 else in browser
*
* @param endPoint of the url
*/
private void openUrl(String endPoint) {
Uri uri;
uri = Uri.parse(app.getServerHostUri() + endPoint);
if (Build.VERSION.SDK_INT < 15) {
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
return;
}
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
CustomTabsIntent intent = builder.build();
intent.launchUrl(ZulipActivity.this, uri);
}
private void dispatchPickIntent() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
// For Api level greater than or equal to 18, allow user to select multiple files
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
}
if (intent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(intent, Constants.REQUEST_PICK_FILE);
// activity transition animation
ActivityTransitionAnim.transition(ZulipActivity.this);
}
}
/**
* Switches the current Day/Night mode to Night/Day mode
*
* @param nightMode which Mode {@link android.support.v7.app.AppCompatDelegate.NightMode}
*/
private void setNightMode(@AppCompatDelegate.NightMode int nightMode) {
AppCompatDelegate.setDefaultNightMode(nightMode);
if (Build.VERSION.SDK_INT >= 11) {
recreate();
}
}
/**
* Log the user out of the app, clearing our cache of their credentials.
*/
private void logout() {
commonProgressDialog.showWithMessage(getString(R.string.logging_out));
this.logged_in = false;
final String serverUrl = app.getServerHostUri();
notifications.logOut(new Runnable() {
public void run() {
app.logOut();
openLogin(serverUrl);
}
});
}
/**
* Switch to the login view.
*/
private void openLogin(String serverUrl) {
if (commonProgressDialog != null && commonProgressDialog.isShowing()) {
commonProgressDialog.dismiss();
}
Intent i = new Intent(this, LoginActivity.class);
i.putExtra(Constants.SERVER_URL, serverUrl);
startActivity(i);
// activity transition animation
ActivityTransitionAnim.transition(this);
finish();
}
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// our display has changed, lets recalculate the spacer
// this.size_bottom_spacer();
drawerToggle.onConfigurationChanged(newConfig);
}
protected void onPause() {
super.onPause();
Log.i("status", "suspend");
unregisterReceiver(onGcmMessage);
if (event_poll != null) {
event_poll.abort();
event_poll = null;
}
if (statusUpdateHandler != null) {
statusUpdateHandler.removeMessages(0);
}
}
protected void onResume() {
super.onResume();
Log.i("status", "resume");
// Set up the BroadcastReceiver to trap GCM messages so notifications
// don't show while in the app
IntentFilter filter = new IntentFilter(GcmBroadcastReceiver.getGCMReceiverAction(getApplicationContext()));
filter.setPriority(2);
registerReceiver(onGcmMessage, filter);
homeList.onActivityResume();
if (narrowedList != null) {
narrowedList.onActivityResume();
}
startRequests();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (statusUpdateHandler != null) {
statusUpdateHandler.removeMessages(0);
statusUpdateHandler.removeCallbacks(statusUpdateRunnable);
}
if (mNotificationManager == null) {
mNotificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
}
mNotificationManager.cancelAll();
if (mCancelHashMap != null) {
for (Call call : mCancelHashMap.values()) {
call.cancel();
}
}
}
/**
* Refresh the current user profile, removes all the tables from the database and reloads them from the server, reset the queue.
*/
private void onRefresh() {
super.onResume();
if (event_poll != null) {
event_poll.abort();
event_poll = null;
}
app.clearConnectionState();
app.resetDatabase();
app.setEmail(app.getYou().getEmail());
startRequests();
}
private void startRequests() {
Log.i("zulip", "Starting requests");
if (event_poll != null) {
event_poll.abort();
event_poll = null;
}
event_poll = new AsyncGetEvents(this);
event_poll.start();
}
public void onReadyToDisplay(boolean registered) {
homeList.onReadyToDisplay(registered);
if (narrowedList != null) {
narrowedList.onReadyToDisplay(registered);
}
}
public void onNewMessages(Message[] messages) {
homeList.onNewMessages(messages);
if (narrowedList != null) {
narrowedList.onNewMessages(messages);
}
if (!currentList.isScrolledToLastMessage())
showSnackbarNotification(messages); //Show notification
}
private void setupSnackBar() {
final CoordinatorLayout coordinatorLayout = (CoordinatorLayout) findViewById(R.id.coordinatorLayout);
TypedValue tv = new TypedValue();
if (getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true))
mToolbarHeightInPx = TypedValue.complexToDimensionPixelSize(tv.data, getResources().getDisplayMetrics());
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
statusBarHeight = getResources().getDimensionPixelSize(resourceId);
}
topSnackBar = new TopSnackBar(this);
coordinatorLayout.addView(topSnackBar.getLinearLayout());
}
private void showSnackbarNotification(Message[] messages) {
MutedTopics mutedTopics = MutedTopics.get();
String notificationMessage;
int nonMutedMessagesCount = 0;
Message tempMessage = null; //Stores a temporary message which is non-muted, later used for retrieving Stream/Topic
for (Message message : messages) {
//Check if all messages from same topic/private and remove all the muted messages or send by user itself
if (mutedTopics.isTopicMute(message) || ((message.getStream() != null) && !message.getStream().getInHomeView())
|| message.getSender().getEmail().equals(app.getYou().getEmail())) {
continue;
}
nonMutedMessagesCount++;
if (prevId != null && !prevId.equals(message.getIdForHolder())) {
prevId = null;
tempMessage = null;
prevMessageSameCount = 0;
break;
} else {
prevMessageSameCount++;
}
prevId = message.getIdForHolder();
if (tempMessage == null) tempMessage = message;
}
if (nonMutedMessagesCount == 0) return;
if (prevId == null && messages.length > 1) {
notificationMessage = getResources().getQuantityString(R.plurals.new_message_mul_sender, nonMutedMessagesCount, nonMutedMessagesCount);
narrowFilter = null;
if (narrowedList != null) {
narrowedList = null;
getSupportFragmentManager().popBackStack(NARROW,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
topSnackBar.setAction(R.string.SHOW, new View.OnClickListener() {
@Override
public void onClick(View view) {
topSnackBar.dismiss();
homeList.showLatestMessages();
}
});
} else {
if (messages.length == 1) tempMessage = messages[0];
String name = (tempMessage.getType() == MessageType.PRIVATE_MESSAGE) ? getString(R.string.notify_private, tempMessage.getSenderFullName()) : getString(R.string.notify_stream, tempMessage.getStream().getName(), tempMessage.getSubject());
if (prevMessageSameCount > 0) name += " (" + prevMessageSameCount + ")";
notificationMessage = getResources().getQuantityString(R.plurals.new_message, nonMutedMessagesCount, nonMutedMessagesCount, name);
narrowFilter = (tempMessage.getType() == MessageType.PRIVATE_MESSAGE) ? new NarrowFilterPM(Arrays.asList(tempMessage.getRecipients(app))) : new NarrowFilterStream(tempMessage.getStream(), tempMessage.getSubject());
topSnackBar.setAction(R.string.SHOW, new View.OnClickListener() {
@Override
public void onClick(View view) {
topSnackBar.dismiss();
doNarrow(narrowFilter);
}
});
}
topSnackBar.show((appBarLayout.getVisibility() == View.GONE) ? statusBarHeight : statusBarHeight + mToolbarHeightInPx, notificationMessage, TopSnackBar.LENGTH_LONG);
}
/**
* Get current message list fragment.
*
* @return {@link MessageListFragment}
*/
public MessageListFragment getCurrentMessageList() {
if (narrowedList == null) {
return homeList;
} else {
return narrowedList;
}
}
/**
* Store floating message header
* useful when message list get's scrolled
* @param viewHolder floating message header
*/
public void setViewHolder(RecyclerView.ViewHolder viewHolder) {
this.viewHolder = viewHolder;
}
// Intent Extra constants
public enum Flag {
RESET_DATABASE,
}
private boolean isTablet() {
return isTablet(this);
}
public static boolean isTablet(Context context) {
return context.getResources().getBoolean(R.bool.isTablet);
}
/**
* get top margin for floating header
*
* @return toolbar height if it is visible else 0
*/
public int getFloatingHeaderTopMargin() {
return (isActionBarShown) ? mToolbarHeightInPx : 0;
}
}