/******************************************************************************* * Copyright 2012 Keith Johnson * * 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 com.ubergeek42.WeechatAndroid; import java.security.cert.CertPath; import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.EnumSet; import java.util.Set; import javax.net.ssl.SSLException; import android.annotation.SuppressLint; import android.support.v4.app.DialogFragment; import android.support.v4.view.MenuItemCompat; import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AlertDialog; import android.support.v7.widget.Toolbar; import android.text.TextUtils; import android.view.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.res.Configuration; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.FragmentManager; import android.support.v4.view.ViewPager; import android.support.v4.widget.DrawerLayout; import android.view.View.OnClickListener; import android.view.inputmethod.InputMethodManager; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import com.ubergeek42.WeechatAndroid.adapters.MainPagerAdapter; import com.ubergeek42.WeechatAndroid.adapters.NickListAdapter; import com.ubergeek42.WeechatAndroid.fragments.BufferFragment; import com.ubergeek42.WeechatAndroid.relay.Buffer; import com.ubergeek42.WeechatAndroid.relay.BufferList; import com.ubergeek42.WeechatAndroid.relay.Nick; import com.ubergeek42.WeechatAndroid.service.P; import com.ubergeek42.WeechatAndroid.service.RelayService; import com.ubergeek42.WeechatAndroid.service.SSLHandler; import com.ubergeek42.WeechatAndroid.utils.InvalidHostnameDialog; import com.ubergeek42.WeechatAndroid.utils.MyMenuItemStuffListener; import com.ubergeek42.WeechatAndroid.utils.ToolbarController; import com.ubergeek42.WeechatAndroid.utils.UntrustedCertificateDialog; import com.ubergeek42.WeechatAndroid.utils.Utils; import com.ubergeek42.WeechatAndroid.service.RelayService.STATE; import static com.ubergeek42.WeechatAndroid.service.Events.*; import static com.ubergeek42.WeechatAndroid.service.RelayService.STATE.*; import de.greenrobot.event.EventBus; public class WeechatActivity extends AppCompatActivity implements CutePagerTitleStrip.CutePageChangeListener { private static Logger logger = LoggerFactory.getLogger("WA"); final private static boolean DEBUG_OPTIONS_MENU = false; final private static boolean DEBUG_LIFECYCLE = true; final private static boolean DEBUG_CONNECTION = false; final private static boolean DEBUG_INTENT = false; final private static boolean DEBUG_BUFFERS = false; final private static boolean DEBUG_DRAWER = false; private Menu uiMenu; private ViewPager uiPager; private MainPagerAdapter adapter; private InputMethodManager imm; private CutePagerTitleStrip uiStrip; private boolean slidy; private boolean drawerEnabled = true; private boolean drawerShowing = false; private DrawerLayout uiDrawerLayout = null; private View uiDrawer = null; private ActionBarDrawerToggle drawerToggle = null; private ImageView uiInfo; public ToolbarController toolbarController; //////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////// life cycle //////////////////////////////////////////////////////////////////////////////////////////////// @Override public void onCreate(@Nullable Bundle savedInstanceState) { logger.debug("onCreate({})", savedInstanceState); // after OOM kill and not going to restore anything? remove all fragments & open buffers if (!P.isServiceAlive() && !BufferList.hasData()) { P.openBuffers.clear(); savedInstanceState = null; } super.onCreate(savedInstanceState); // load layout setContentView(R.layout.main_screen); // remove window color so that we get low overdraw getWindow().setBackgroundDrawableResource(android.R.color.transparent); // prepare pager FragmentManager manager = getSupportFragmentManager(); uiPager = (ViewPager) findViewById(R.id.main_viewpager); adapter = new MainPagerAdapter(manager, uiPager); uiPager.setAdapter(adapter); // prepare action bar setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); final ActionBar uiActionBar = getSupportActionBar(); //noinspection ConstantConditions uiActionBar.setHomeButtonEnabled(true); uiActionBar.setDisplayShowCustomEnabled(true); uiActionBar.setDisplayShowTitleEnabled(false); LayoutInflater inflater = (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE); uiStrip = (CutePagerTitleStrip) inflater.inflate(R.layout.cute_pager_title_strip_layout, null); uiStrip.setViewPager(uiPager); uiStrip.setOnPageChangeListener(this); uiActionBar.setCustomView(uiStrip); // this is the text view behind the uiPager // it says stuff like 'connecting', 'disconnected' et al uiInfo = (ImageView) findViewById(R.id.kitty); uiInfo.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (state.contains(STARTED)) disconnect(); else connect(); } }); // if this is true, we've got notification drawer and have to deal with it // setup drawer toggle, which calls drawerVisibilityChanged() slidy = getResources().getBoolean(R.bool.slidy); uiDrawer = findViewById(R.id.bufferlist_fragment); if (slidy) { uiDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); drawerToggle = new ActionBarDrawerToggle(this, uiDrawerLayout, R.string.open_drawer, R.string.close_drawer) { @SuppressWarnings("SimplifiableConditionalExpression") @Override public void onDrawerStateChanged(int newState) { super.onDrawerStateChanged(newState); boolean showing = (newState == DrawerLayout.STATE_IDLE) ? uiDrawerLayout.isDrawerVisible(uiDrawer) : true; if (drawerShowing != showing) drawerVisibilityChanged(showing); } }; drawerShowing = uiDrawerLayout.isDrawerVisible(uiDrawer); uiDrawerLayout.setDrawerListener(drawerToggle); uiActionBar.setDisplayHomeAsUpEnabled(true); } toolbarController = new ToolbarController(this); imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); String title = "WA v" + BuildConfig.VERSION_NAME; setTitle(title); uiStrip.setEmptyText(title); updateCutePagerTitleStrip(); if (P.isServiceAlive()) connect(); logger.info("onCreate(): service alive? {}; have static data? {}; P.openBuffers: {}; fragments: {}", P.isServiceAlive(), BufferList.getBufferList().size() != 0, P.openBuffers, manager.getFragments()); // restore buffers if we have data in the static // if no data and not going to connect, clear stuff // if no data and going to connect, let the LISTED event restore it all if (adapter.canRestoreBuffers()) adapter.restoreBuffers(); } public void connect() { P.loadConnectionPreferences(); String error = P.validateConnectionPreferences(); if (error != null) { Toast.makeText(getBaseContext(), error, Toast.LENGTH_LONG).show(); return; } logger.debug("connect()"); Intent i = new Intent(this, RelayService.class); i.setAction(RelayService.ACTION_START); startService(i); } public void disconnect() { logger.debug("disconnect()"); Intent i = new Intent(this, RelayService.class); i.setAction(RelayService.ACTION_STOP); startService(i); } //////////////////////////////////////////////////////////////////////////////////////////////// @Override protected void onStart() { if (DEBUG_LIFECYCLE) logger.debug("onStart()"); super.onStart(); state = null; EventBus.getDefault().registerSticky(this); if (getIntent().hasExtra(EXTRA_NAME)) openBufferFromIntent(); updateHotCount(BufferList.getHotCount()); } @Override protected void onStop() { if (DEBUG_LIFECYCLE) logger.debug("onStop()"); EventBus.getDefault().unregister(this); P.saveStuff(); super.onStop(); } @Override protected void onDestroy() { if (DEBUG_LIFECYCLE) logger.debug("onDestroy()"); super.onDestroy(); } //////////////////////////////////////////////////////////////////////////////////////////////// these two are necessary for the drawer @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); if (slidy) drawerToggle.syncState(); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); if (slidy) drawerToggle.onConfigurationChanged(newConfig); } //////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////// the joy //////////////////////////////////////////////////////////////////////////////////////////////// private void adjustUI() { logger.debug("adjustUI()"); int image = R.drawable.ic_big_connecting; if (state.contains(STOPPED)) image = R.drawable.ic_big_disconnected; else if (state.contains(AUTHENTICATED)) image = R.drawable.ic_big_connected; setInfoImage(image); setDrawerEnabled(state.contains(LISTED)); makeMenuReflectConnectionStatus(); } //////////////////////////////////////////////////////////////////////////////////////////////// events? private EnumSet<STATE> state = null; @SuppressWarnings("unused") public void onEvent(StateChangedEvent event) { logger.debug("onEvent({})", event); boolean init = state == null; state = event.state; adjustUI(); if (state.contains(LISTED)) { if (adapter.canRestoreBuffers()) runOnUiThread(new Runnable() {public void run() {adapter.restoreBuffers();}}); else if (!init && slidy) showDrawerIfPagerIsEmpty(); } } @SuppressWarnings("unused") public void onEvent(final ExceptionEvent event) { if (DEBUG_CONNECTION) logger.debug("onEvent({})", event); final Exception e = event.e; if (e instanceof SSLException) { SSLException e1 = (SSLException) e; if (e1.getCause() instanceof CertificateException) { CertificateException e2 = (CertificateException) e1.getCause(); if (e2.getCause() instanceof CertPathValidatorException) { CertPathValidatorException e3 = (CertPathValidatorException) e2.getCause(); CertPath cp = e3.getCertPath(); final X509Certificate certificate = (X509Certificate) cp.getCertificates().get(0); DialogFragment f; if (SSLHandler.checkHostname(P.host, P.port)) { // valid hostname, untrusted certificate f = UntrustedCertificateDialog.newInstance(certificate); } else { // invalid hostname, abort early final Set<String> hosts = SSLHandler.getCertificateHosts(certificate); // remove the host itself, in case the host is an IP defined in the // certificate 'Common Name' (Android does not accept that) hosts.remove(P.host); f = InvalidHostnameDialog.newInstance(P.host, hosts); } f.show(getSupportFragmentManager(), "ssl-error"); disconnect(); return; } } } final String msg = getString(R.string.error, TextUtils.isEmpty(e.getMessage()) ? e.getClass().getSimpleName() : e.getMessage()); runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(getBaseContext(), msg, Toast.LENGTH_LONG).show(); } }); } //////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////// OnPageChangeListener //////////////////////////////////////////////////////////////////////////////////////////////// @Override public void onPageScrollStateChanged(int state) {} @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {} @Override public void onPageSelected(int position) { hideSoftwareKeyboard(); toolbarController.onPageChangedOrSelected(); } @Override public void onChange() { updateMenuItems(); hideSoftwareKeyboard(); toolbarController.onPageChangedOrSelected(); int visible = uiPager.getAdapter().getCount() == 0 ? View.VISIBLE : View.GONE; findViewById(R.id.kitty_background).setVisibility(visible); findViewById(R.id.kitty).setVisibility(visible); } //////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////// MENU //////////////////////////////////////////////////////////////////////////////////////////////// volatile private int hotNumber = 0; private @Nullable TextView uiHot = null; /** update hot count (that red square over the bell icon) at any time ** also sets "hotNumber" in case menu has to be recreated ** can be called off the main thread */ public void updateHotCount(final int newHotNumber) { if (DEBUG_OPTIONS_MENU) logger.debug("updateHotCount(), hot: {} -> {}", hotNumber, newHotNumber); hotNumber = newHotNumber; if (uiHot != null) uiHot.post(new Runnable() { @Override @SuppressLint("SetTextI18n") public void run() { if (newHotNumber == 0) uiHot.setVisibility(View.INVISIBLE); else { uiHot.setVisibility(View.VISIBLE); uiHot.setText(Integer.toString(newHotNumber)); } } }); } /** hide or show nicklist/close menu item according to buffer ** MUST be called on main thread */ private void updateMenuItems() { if (uiMenu == null) return; boolean bufferVisible = adapter.getCount() > 0; uiMenu.findItem(R.id.menu_nicklist).setVisible(bufferVisible); uiMenu.findItem(R.id.menu_close).setVisible(bufferVisible); } /** Can safely hold on to this according to docs ** http://developer.android.com/reference/android/app/Activity.html#onCreateOptionsMenu(android.view.Menu) **/ @Override public boolean onCreateOptionsMenu(final Menu menu) { if (DEBUG_OPTIONS_MENU) logger.debug("onCreateOptionsMenu(...)"); MenuInflater menuInflater = getMenuInflater(); menuInflater.inflate(R.menu.menu_actionbar, menu); final View menuHotlist = MenuItemCompat.getActionView(menu.findItem(R.id.menu_hotlist)); uiHot = (TextView) menuHotlist.findViewById(R.id.hotlist_hot); updateHotCount(hotNumber); new MyMenuItemStuffListener(menuHotlist, getString(R.string.hint_show_hot_message)) { @Override public void onClick(View v) { onHotlistSelected(); } }; this.uiMenu = menu; updateMenuItems(); makeMenuReflectConnectionStatus(); return super.onCreateOptionsMenu(menu); } /** handle the options when the user presses the menu button */ @Override public boolean onOptionsItemSelected(MenuItem item) { if (DEBUG_OPTIONS_MENU) logger.debug("onOptionsItemSelected({})", item); switch (item.getItemId()) { case android.R.id.home: { if (slidy && drawerEnabled) { if (drawerShowing) hideDrawer(); else showDrawer(); } break; } case R.id.menu_connection_state: { if (state.contains(STARTED)) disconnect(); else connect(); break; } case R.id.menu_preferences: { Intent intent = new Intent(this, PreferencesActivity.class); startActivity(intent); break; } case R.id.menu_close: { BufferFragment current = adapter.getCurrentBufferFragment(); if (current != null) current.onBufferClosed(); break; } case R.id.menu_hotlist: break; case R.id.menu_nicklist: final Buffer buffer = BufferList.findByFullName(adapter.getCurrentBufferFullName()); if (buffer == null) break; final NickListAdapter nicklistAdapter = new NickListAdapter(WeechatActivity.this, buffer); AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setAdapter(nicklistAdapter, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int position) { Nick nick = nicklistAdapter.getItem(position); EventBus.getDefault().post(new SendMessageEvent(String.format("input %s /query %s", buffer.hexPointer(), nick.name))); } }); AlertDialog dialog = builder.create(); dialog.setTitle("squirrels are awesome"); dialog.setOnShowListener(nicklistAdapter); dialog.setOnDismissListener(nicklistAdapter); dialog.show(); break; } return super.onOptionsItemSelected(item); } private void onHotlistSelected() { if (DEBUG_OPTIONS_MENU) logger.debug("onHotlistSelected()"); Buffer buffer = BufferList.getHotBuffer(); if (buffer != null) openBuffer(buffer.fullName); else Toast.makeText(this, getString(R.string.no_hot_buffers), Toast.LENGTH_SHORT).show(); } /** change first menu item from connect to disconnect or back depending on connection status */ private void makeMenuReflectConnectionStatus() { if (DEBUG_OPTIONS_MENU) logger.debug("makeMenuReflectConnectionStatus()"); runOnUiThread(new Runnable() { @Override public void run() { if (uiMenu != null) { MenuItem connectionStatus = uiMenu.findItem(R.id.menu_connection_state); String msg; if (state.contains(AUTHENTICATED)) msg = getString(R.string.disconnect); else if (state.contains(STARTED)) msg = getString(R.string.stop_connecting); else msg = getString(R.string.connect); connectionStatus.setTitle(msg); final View menuHotlist = MenuItemCompat.getActionView(uiMenu.findItem(R.id.menu_hotlist)); ImageView bellImage = (ImageView) menuHotlist.findViewById(R.id.hotlist_bell); bellImage.setImageResource(P.optimizeTraffic ? R.drawable.ic_bell_cracked : R.drawable.ic_bell); } } }); } //////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////// MISC //////////////////////////////////////////////////////////////////////////////////////////////// public void openBuffer(@NonNull String fullName) { openBuffer(fullName, null); } public void openBuffer(@NonNull final String fullName, final String text) { if (DEBUG_BUFFERS) logger.debug("openBuffer({})", fullName); if (adapter.isBufferOpen(fullName) || state.contains(AUTHENTICATED)) { adapter.openBuffer(fullName); adapter.focusBuffer(fullName); if (text != null) { uiDrawer.post(new Runnable() { @Override public void run() { adapter.setBufferInputText(fullName, text); } }); } if (slidy) hideDrawer(); } else { Toast.makeText(this, getString(R.string.not_connected), Toast.LENGTH_SHORT).show(); } } // In own thread to prevent things from breaking public void closeBuffer(String fullName) { if (DEBUG_BUFFERS) logger.debug("closeBuffer({})", fullName); adapter.closeBuffer(fullName); if (slidy) showDrawerIfPagerIsEmpty(); } /** hides the software keyboard, if any */ public void hideSoftwareKeyboard() { imm.hideSoftInputFromWindow(uiPager.getWindowToken(), 0); } @Override public void onBackPressed() { if (DEBUG_LIFECYCLE) logger.debug("onBackPressed()"); if (slidy && drawerShowing) hideDrawer(); else super.onBackPressed(); } /** called if the text of one of the buffers has been changed ** and the uiStrip doesn't update itself because there's no scrolling */ public void updateCutePagerTitleStrip() { uiStrip.updateText(); } //////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////// drawer stuff //////////////////////////////////////////////////////////////////////////////////////////////// public void drawerVisibilityChanged(boolean showing) { if (DEBUG_DRAWER) logger.debug("drawerVisibilityChanged({})", showing); drawerShowing = showing; hideSoftwareKeyboard(); BufferFragment current = adapter.getCurrentBufferFragment(); if (current != null) current.maybeChangeVisibilityState(); } public boolean isPagerNoticeablyObscured() { return drawerShowing; //todo? } private void setDrawerEnabled(final boolean enabled) { if (DEBUG_DRAWER) logger.debug("setDrawerEnabled({})", enabled); drawerEnabled = enabled; uiPager.post(new Runnable() { @Override public void run() { if (slidy) uiDrawerLayout.setDrawerLockMode(enabled ? DrawerLayout.LOCK_MODE_UNLOCKED : DrawerLayout.LOCK_MODE_LOCKED_CLOSED); else uiDrawer.setVisibility(enabled ? View.VISIBLE : View.GONE); } }); } public void showDrawer() { if (DEBUG_DRAWER) logger.debug("showDrawer()"); if (!drawerEnabled) return; if (!drawerShowing) drawerVisibilityChanged(true); // we need this so that drawerShowing is set immediately uiPager.post(new Runnable() { @Override public void run() { uiDrawerLayout.openDrawer(uiDrawer); } }); } public void hideDrawer() { if (DEBUG_DRAWER) logger.debug("hideDrawer()"); uiPager.post(new Runnable() { @Override public void run() { uiDrawerLayout.closeDrawer(uiDrawer); } }); } /** pop up drawer if connected & no pages in the adapter **/ public void showDrawerIfPagerIsEmpty() { if (DEBUG_DRAWER) logger.debug("showDrawerIfPagerIsEmpty()"); if (!drawerShowing) uiPager.post(new Runnable() { @Override public void run() { if (state.contains(LISTED) && adapter.getCount() == 0) showDrawer(); } }); } /** set image that appears in the pager when no pages are open */ @SuppressWarnings("deprecation") private void setInfoImage(final int id) { final Drawable drawable = getResources().getDrawable(id); uiInfo.post(new Runnable() { @Override @SuppressWarnings("ConstantConditions") public void run() { Utils.setImageDrawableWithFade(uiInfo, drawable, 350); } }); } //////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////// intent //////////////////////////////////////////////////////////////////////////////////////////////// public final static String EXTRA_NAME = "full_name"; /** we may get intent while we are connected to the service and when we are not. ** empty (but present) fullName means open the drawer (in case we have highlights ** on multiple buffers */ @Override protected void onNewIntent(Intent intent) { if (DEBUG_INTENT) logger.debug("onNewIntent(...), fullName='{}'", intent.getStringExtra(EXTRA_NAME)); super.onNewIntent(intent); if (intent.hasExtra(EXTRA_NAME)) { setIntent(intent); openBufferFromIntent(); } } /** the extra must be non-null */ private void openBufferFromIntent() { if (DEBUG_INTENT) logger.debug("openBufferFromIntent()"); String name = getIntent().getStringExtra(EXTRA_NAME); if ("".equals(name)) { if (slidy) showDrawer(); } else { String text = getIntent().getStringExtra(Intent.EXTRA_TEXT); openBuffer(name, text); if (text != null) { getIntent().removeExtra(Intent.EXTRA_TEXT); } } getIntent().removeExtra(EXTRA_NAME); } }