info.tellmetime.TellmetimeActivity.java Source code

Java tutorial

Introduction

Here is the source code for info.tellmetime.TellmetimeActivity.java

Source

/*
 * Copyright (c) 2014 Samuel Jarosiski <samueljarosinski@gmail.com>
 *
 * This file is part of "Tell me time".
 * http://tellmetime.info
 *
 * "Tell me time" is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * "Tell me time" is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with "Tell me time". If not, see <http://www.gnu.org/licenses/>.
 */

package info.tellmetime;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ArgbEvaluator;
import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.WallpaperManager;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Shader;
import android.graphics.Typeface;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.provider.MediaStore;
import android.support.v4.view.MotionEventCompat;
import android.support.v7.widget.PopupMenu;
import android.util.Property;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Calendar;
import java.util.Timer;
import java.util.TimerTask;

public class TellmetimeActivity extends Activity implements View.OnTouchListener, SeekBar.OnSeekBarChangeListener,
        PopupMenu.OnMenuItemClickListener, AdapterView.OnItemSelectedListener {

    public static final String HIGHLIGHT = "highlight";
    public static final String BACKLIGHT = "backlight";
    public static final String BACKGROUND = "background";
    public static final String BACKGROUND_MODE = "background_mode";
    public static final String BACKGROUND_MODE_TOGGLED = "background_mode_toggled";
    public static final String POSITION = "position";
    public static final String MINUTES_SIZE = "minutes_size";
    public static final String IMAGE_FILE_NAME = "image";
    public static final String NIGHTMODE = "night_mode";

    public static final int MODE_BACKGROUND_SOLID = 0;
    public static final int MODE_BACKGROUND_WALLPAPER = 1;
    public static final int MODE_BACKGROUND_IMAGE = 2;

    private static final int REQUEST_CODE = 1;

    /**
     * The {@link android.widget.RelativeLayout} which is the root layout of main activity.
     */
    private RelativeLayout mSurface;

    /**
     * Background picture displaying either current wallpaper or user selected image.
     */
    private ImageView mBackgroundImage;

    /**
     * The {@link android.widget.SeekBar} for adjusting highlight color.
     */
    private SeekBar mSeekBarHighlight;

    /**
     * The instance of the {@link PanelHider} for this activity.
     */
    private PanelHider mHider;

    /**
     * The shorter edge of the Activity window.
     */
    private int mShorterEdge;

    /**
     * The density of the screen as scaling factor from 160dpi.
     */
    private float mDensity;

    /**
     * The current highlight color used in the clock.
     */
    private int mHighlightColor;

    /**
     * The current dim color used in the clock.
     */
    private int mBacklightColor;

    /**
     * Current background mode;
     */
    private int mBackgroundMode = MODE_BACKGROUND_SOLID;

    /**
     * The current background color represented as its Integer value.
     */
    private int mBackgroundColor;

    /**
     * Cached background image.
     */
    private Bitmap mImage;

    /** Array with cached Hue, Saturation and Value of background color.
     * {0..360, 0..1, 0..1}
     */
    private final float[] mHSV = new float[3];

    /**
     * Value indicating relative position of #mSeekBarHighlight.
     */
    private float mHighlightPosition = 0.0f;

    /**
     * Size of minutes indicators in DP.
     */
    private int mMinutesSize = 36;

    /**
     * Cached bitmap representing colors in #mSeekBarHighlight, used to retrieve color at #mSeekBarHighlight position.
     */
    private Bitmap mRainbow;

    /**
     * Flag indicating whether screen is at full brightness or is dimmed.
     */
    private boolean isNightMode;

    // Distance in pixels that needs to be exceeded to recognize an event as a actual moving
    // and not as just tapping the screen.
    // (Often touch interaction cause at least one MotionEvent.ACTION_MOVE signal).
    private int mTouchSlop;

    // Cached values.
    private int mScreenWidth;
    private int mScreenHeight;

    private int mBacklightLight;
    private int mBacklightDark;

    private SharedPreferences mSettings = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_tellmetime);

        mDensity = getResources().getDisplayMetrics().density;
        mScreenWidth = getResources().getDisplayMetrics().widthPixels;
        mScreenHeight = getResources().getDisplayMetrics().heightPixels;
        mShorterEdge = Math.min(mScreenWidth, mScreenHeight);
        mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
        mBacklightLight = getResources().getColor(R.color.backlight_light);
        mBacklightDark = getResources().getColor(R.color.backlight_dark);

        // Restore background and highlight colors from saved values or set defaults.
        mSettings = getSharedPreferences("PREFS", Context.MODE_PRIVATE);
        mHighlightColor = mSettings.getInt(HIGHLIGHT, Color.WHITE);
        mBacklightColor = mSettings.getInt(BACKLIGHT, mBacklightLight);
        mBackgroundColor = mSettings.getInt(BACKGROUND, getResources().getColor(R.color.background));
        mBackgroundMode = mSettings.getInt(BACKGROUND_MODE, MODE_BACKGROUND_SOLID);
        mHighlightPosition = mSettings.getFloat(POSITION, 0.0f);
        mMinutesSize = mSettings.getInt(MINUTES_SIZE, 36);
        isNightMode = mSettings.getBoolean(NIGHTMODE, false);

        // Dim the navigation bar.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH)
            getWindow().getDecorView().setSystemUiVisibility(isNightMode ? View.SYSTEM_UI_FLAG_LOW_PROFILE : 0);

        mSurface = (RelativeLayout) findViewById(R.id.surface);
        mSurface.setBackgroundColor(mBackgroundColor);

        resizeClock();

        Typeface mTypefaceBold = Typeface.createFromAsset(getAssets(), "Roboto-BoldCondensed.ttf");

        // Set typeface of all items in the clock to Roboto and dim each one and drop shadow on them.
        final LinearLayout mClock = (LinearLayout) findViewById(R.id.clock);
        for (int i = 0; i < mClock.getChildCount(); i++) {
            LinearLayout row = (LinearLayout) mClock.getChildAt(i);

            for (int j = 0; j < row.getChildCount(); j++) {
                TextView tv = (TextView) row.getChildAt(j);

                tv.setTypeface(mTypefaceBold);
                tv.setTextColor(mBacklightColor);
                tv.setShadowLayer(mShorterEdge / 200 * mDensity, 0, 0, mBacklightColor);
            }
        }

        ViewGroup minutesDots = (ViewGroup) findViewById(R.id.minutes_dots);
        for (int i = 0; i < minutesDots.getChildCount(); i++) {
            TextView m = (TextView) minutesDots.getChildAt(i);

            m.setTypeface(mTypefaceBold);
            m.setTextColor(mBacklightColor);
            m.setShadowLayer(mShorterEdge / 200 * mDensity, 0, 0, mBacklightColor);
        }

        // Set Roboto font on TextView where it isn't default.
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
            Typeface mTypefaceItalic = Typeface.createFromAsset(getAssets(), "Roboto-CondensedItalic.ttf");

            ((TextView) findViewById(R.id.labelBackground)).setTypeface(mTypefaceBold);
            ((TextView) findViewById(R.id.info_background)).setTypeface(mTypefaceItalic);
            ((TextView) findViewById(R.id.info_image)).setTypeface(mTypefaceItalic);
            ((TextView) findViewById(R.id.labelHighlight)).setTypeface(mTypefaceBold);
            ((TextView) findViewById(R.id.labelBackground)).setTypeface(mTypefaceBold);
            ((TextView) findViewById(R.id.radio_backlight_light)).setTypeface(mTypefaceBold);
            ((TextView) findViewById(R.id.radio_backlight_dark)).setTypeface(mTypefaceBold);
            ((TextView) findViewById(R.id.radio_backlight_highlight)).setTypeface(mTypefaceBold);
            ((TextView) findViewById(R.id.labelMinutes)).setTypeface(mTypefaceBold);
            ((TextView) findViewById(R.id.m1)).setTypeface(mTypefaceBold);
            ((TextView) findViewById(R.id.m2)).setTypeface(mTypefaceBold);
            ((TextView) findViewById(R.id.m3)).setTypeface(mTypefaceBold);
            ((TextView) findViewById(R.id.m4)).setTypeface(mTypefaceBold);
        }

        FrameLayout mTouchZone = (FrameLayout) findViewById(R.id.touchZone);
        mTouchZone.setOnTouchListener(this);
        mTouchZone.setBackgroundColor(
                getResources().getColor(isNightMode ? R.color.night_mode_overlay : android.R.color.transparent));

        mBackgroundImage = (ImageView) findViewById(R.id.background_image);
        switchBackgroundMode(mBackgroundMode);

        RelativeLayout mPanel = (RelativeLayout) findViewById(R.id.panel);
        mPanel.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                mHider.delayedHide(4000);
                return true;
            }
        });

        mHider = new PanelHider(mPanel, this);

        Spinner spinnerBackgroundMode = (Spinner) findViewById(R.id.spinnerBackgroundMode);
        ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.backgrounds_modes,
                android.R.layout.simple_spinner_item);
        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        spinnerBackgroundMode.setAdapter(adapter);
        spinnerBackgroundMode.setOnItemSelectedListener(this);
        spinnerBackgroundMode.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                mHider.showNoAutoHide();
                return false;
            }
        });
        spinnerBackgroundMode.setSelection(mBackgroundMode);

        mSeekBarHighlight = (SeekBar) findViewById(R.id.highlightValue);
        mSeekBarHighlight.setOnSeekBarChangeListener(this);

        // Draw rainbow gradient on #mSeekBarHighlight and set position.
        drawRainbow();

        if (mBacklightColor == mBacklightLight)
            ((RadioButton) findViewById(R.id.radio_backlight_light)).setChecked(true);
        else if (mBacklightColor == mBacklightDark)
            ((RadioButton) findViewById(R.id.radio_backlight_dark)).setChecked(true);
        else
            ((RadioButton) findViewById(R.id.radio_backlight_highlight)).setChecked(true);

        SeekBar mSeekBarMinutes = (SeekBar) findViewById(R.id.minutesSize);
        mSeekBarMinutes.setOnSeekBarChangeListener(this);
        mSeekBarMinutes.setProgress(mMinutesSize);

        mHider.hideNow();

        Color.colorToHSV(mBackgroundColor, mHSV);
        mHSV[1] = 1.0f;

        //Trigger initial tick.
        mClockAlgorithm.tickTock();

        // Schedule the clock algorithm to tick every round minute.
        Calendar time = Calendar.getInstance();
        time.set(Calendar.MILLISECOND, 0);
        time.set(Calendar.SECOND, 0);
        time.add(Calendar.MINUTE, 1);

        Timer timer = new Timer();
        timer.schedule(mClockTask, time.getTime(), 60 * 1000);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            mSurface.setAlpha(0.0f);
            mSurface.animate().alpha(1.0f).setDuration(1500);
        }

        // If it is first run, hint to user that panel is available.
        if (!mSettings.contains(HIGHLIGHT))
            showToast(R.string.info_first_run);
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            boolean enableTranslucentNavigation = true;
            if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
                    && newConfig.smallestScreenWidthDp < 600)
                enableTranslucentNavigation = false;

            if (enableTranslucentNavigation)
                getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
            else
                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
        }

        mScreenWidth = getResources().getDisplayMetrics().widthPixels;
        mScreenHeight = getResources().getDisplayMetrics().heightPixels;
        mShorterEdge = Math.min(mScreenWidth, mScreenHeight);

        resizeClock();

        mClockAlgorithm.tickTock();
    }

    /**
     * Called when Activity is about to lose focus, so most likely the user is leaving.
     */
    @Override
    protected void onPause() {
        super.onPause();

        // Send current highlight color to ClockService, so widget can be updated with new color.
        Intent intent = new Intent(getApplicationContext(), ClockService.class);
        startService(intent);

        SharedPreferences.Editor editor = mSettings.edit();

        editor.putInt(BACKGROUND, mBackgroundColor);
        editor.putInt(HIGHLIGHT, mHighlightColor);
        editor.putInt(BACKLIGHT, mBacklightColor);
        editor.putInt(BACKGROUND_MODE, mBackgroundMode);
        editor.putFloat(POSITION, mHighlightPosition);
        editor.putInt(MINUTES_SIZE, mMinutesSize);
        editor.putBoolean(NIGHTMODE, isNightMode);

        editor.commit();
    }

    // Cached coordinates of the starting point of new touch gesture, i.e. when the user
    // touched the screen and started to move or is just tapping the screen.
    float mStartX = 0, mStartY = 0;

    // Flag that is set if the touch event was recognized as a movement rather than just a tap.
    boolean isSlopped = false;

    // Flag indicating whether day/night mode was toggled. If set, panel visibility shouldn't be toggled.
    boolean wasModeToggled = false;

    // Handler responsible for long pressing.
    final Handler mLongPressHandler = new Handler();
    Runnable mLongPressed = new Runnable() {
        public void run() {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
                toggleMode();
        }
    };

    /**
     * Processes touch events for FrameLayout overlaying whole activity.
     *
     * If the touch was not on the panel then it changes background color according to the touch
     * position as long as the touch event was recognized as a movement,
     * or toggles panel visibility if it was a tap.
     *
     * @return true if the touch event was consumed.
     */
    @Override
    public boolean onTouch(View view, MotionEvent event) {
        // Coordinates of current touch point.
        final float rawX = event.getRawX();
        final float rawY = event.getRawY();

        switch (MotionEventCompat.getActionMasked(event)) {
        case (MotionEvent.ACTION_DOWN):
            // This action signals that a new touch gesture begins, so we cache its starting point.
            mStartX = rawX;
            mStartY = rawY;

            // Schedule a Runnable to run in 1 second.
            mLongPressHandler.postDelayed(mLongPressed, 1000);

            return true;
        case (MotionEvent.ACTION_MOVE):
            // Distance between starting point and current one.
            final double distance = Math.sqrt(Math.pow(rawX - mStartX, 2) + Math.pow(rawY - mStartY, 2));

            if (distance > mTouchSlop) {
                // If the user exceeded the slop value, then we start to track the movement.
                isSlopped = true;

                // Cancel scheduled Runnable.
                mLongPressHandler.removeCallbacks(mLongPressed);

                if (mHider.isVisible()) {
                    // Hide panel to present only the clock.
                    mHider.hide();
                } else {
                    if (mBackgroundMode != MODE_BACKGROUND_SOLID)
                        return true;

                    // Change the background color according to the touch position.

                    if (event.getPointerCount() > 1) {
                        // If the user is using more than one finger, then we change saturation.

                        // Saturation depends on top <-> bottom movement of second finger.
                        mHSV[1] = MotionEventCompat.getY(event, 1) / mScreenHeight;
                    } else {
                        // If it's a single finger gesture, then we change hue and value.

                        // Hue depends on left <-> right movement.
                        mHSV[0] = (float) Math.floor(rawX / mScreenWidth * 360);
                        // Value depends on up <-> down movement.
                        mHSV[2] = rawY / mScreenHeight;
                    }

                    mBackgroundColor = Color.HSVToColor(mHSV);
                    mSurface.setBackgroundColor(mBackgroundColor);
                }
            } else {
                // If required distance is not exceeded, we only unset the slop flag.
                isSlopped = false;
            }

            return true;
        case (MotionEvent.ACTION_UP):
            mLongPressHandler.removeCallbacks(mLongPressed);

            // This action ends every touch interaction, so if the event was just a tap,
            // i.e. distance hasn't exceed the slop value, then it concerns panel.

            if (!isSlopped && !wasModeToggled) {
                // If the touch gesture was just a tap then toggle panel visibility.
                mHider.toggle();
            }

            // As the gesture ended, we need to unset the slop and toggle flag.
            isSlopped = false;
            wasModeToggled = false;

            return true;
        case (MotionEvent.ACTION_POINTER_UP):
            // Action occurs when second or farther finger goes up.

            isSlopped = true;

            return true;
        default:
            return super.onTouchEvent(event);
        }
    }

    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        // Delay hiding the panel. This is to prevent the jarring behavior of
        // controls going away while interacting with UI.
        mHider.delayedHide(4000);

        FrameLayout frameBackgroundMode = (FrameLayout) findViewById(R.id.frameBackgroundMode);
        for (int i = 0; i < frameBackgroundMode.getChildCount(); i++)
            frameBackgroundMode.getChildAt(i).setVisibility(View.GONE);
        frameBackgroundMode.getChildAt(position).setVisibility(View.VISIBLE);

        mBackgroundMode = position;

        switchBackgroundMode(position);
    }

    @Override
    public void onNothingSelected(AdapterView<?> parent) {
    }

    /**
     * Handles changing highlight color via #mSeekBarHighlight.
     *
     * @param value indicates exact offset in gradient (SeekBar's max value is equal to #mRainbow width).
     */
    @Override
    public void onProgressChanged(SeekBar seekBar, int value, boolean fromUser) {
        switch (seekBar.getId()) {
        case R.id.highlightValue:
            mHighlightColor = mRainbow.getPixel(value, 0);
            if (((RadioButton) findViewById(R.id.radio_backlight_highlight)).isChecked()) {
                float[] highlightHSV = new float[3];
                Color.colorToHSV(mHighlightColor, highlightHSV);
                mBacklightColor = Color.HSVToColor(33, highlightHSV);
            }
            if (fromUser)
                mHighlightPosition = (float) value / seekBar.getMax();

            mClockAlgorithm.tickTock();

            break;

        case R.id.minutesSize:
            mMinutesSize = value;

            if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
                FrameLayout minutesIndicators = (FrameLayout) findViewById(R.id.minutes_indicators);
                RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) minutesIndicators
                        .getLayoutParams();
                params.topMargin = (int) -TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mMinutesSize / 3,
                        getResources().getDisplayMetrics());
                minutesIndicators.setLayoutParams(params);
            }

            ViewGroup minutesDots = (ViewGroup) findViewById(R.id.minutes_dots);
            for (int i = 0; i < minutesDots.getChildCount(); i++) {
                TextView m = (TextView) minutesDots.getChildAt(i);
                m.setTextSize(TypedValue.COMPLEX_UNIT_DIP, mMinutesSize);
            }

            break;
        }

        mHider.delayedHide(4000);
    }

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {
    }

    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {
    }

    public void onBacklightRadioClick(View view) {
        boolean isChecked = ((RadioButton) view).isChecked();

        switch (view.getId()) {
        case R.id.radio_backlight_light:
            if (isChecked)
                mBacklightColor = mBacklightLight;
            break;
        case R.id.radio_backlight_dark:
            if (isChecked)
                mBacklightColor = mBacklightDark;
            break;
        case R.id.radio_backlight_highlight:
            if (isChecked) {
                float[] highlightHSV = new float[3];
                Color.colorToHSV(mHighlightColor, highlightHSV);
                mBacklightColor = Color.HSVToColor(33, highlightHSV);
            }
            break;
        }

        mClockAlgorithm.tickTock();
        mHider.delayedHide(4000);
    }

    public void showPopupMenu(View view) {
        PopupMenu popup = new PopupMenu(this, view);

        popup.setOnMenuItemClickListener(this);
        popup.setOnDismissListener(new PopupMenu.OnDismissListener() {
            @Override
            public void onDismiss(PopupMenu popupMenu) {
                mHider.delayedHide(4000);
            }
        });

        popup.getMenuInflater().inflate(R.menu.menu, popup.getMenu());

        MenuItem toggle = popup.getMenu().findItem(R.id.action_mode);
        toggle.setTitle(isNightMode ? R.string.action_mode_day : R.string.action_mode_night);
        toggle.setIcon(isNightMode ? R.drawable.ic_action_brightness_high : R.drawable.ic_action_brightness_low);

        popup.show();

        mHider.showNoAutoHide();
    }

    @Override
    public boolean onMenuItemClick(MenuItem menuItem) {
        switch (menuItem.getItemId()) {
        case R.id.action_mode:
            if (!mSettings.contains(BACKGROUND_MODE_TOGGLED))
                showToast(R.string.info_toggle_mode);

            toggleMode();

            mHider.hide();
            return true;
        case R.id.action_share_screenshot:
            mHider.hideNow();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        View rootView = getWindow().getDecorView().getRootView();
                        rootView.setDrawingCacheEnabled(true);
                        Bitmap screenshot = Bitmap.createBitmap(rootView.getDrawingCache());
                        rootView.setDrawingCacheEnabled(false);

                        showToast(R.string.info_saving_screenshot);

                        ContentValues values = new ContentValues();
                        values.put(MediaStore.Images.Media.TITLE, getString(R.string.app_name));
                        values.put(MediaStore.Images.Media.DISPLAY_NAME, getString(R.string.app_name) + ".jpg");
                        values.put(MediaStore.Images.Media.MIME_TYPE, "image/*");
                        Uri uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

                        OutputStream outputStream;
                        outputStream = getContentResolver().openOutputStream(uri);
                        screenshot.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
                        outputStream.close();
                        screenshot.recycle();

                        Intent intent = new Intent(Intent.ACTION_SEND);
                        intent.setType("image/jpeg");
                        intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.app_name));
                        String shareText = getString(R.string.app_description)
                                + " Get it at http://tellmetime.info";
                        intent.putExtra(Intent.EXTRA_TEXT, shareText);
                        intent.putExtra(Intent.EXTRA_STREAM, uri);
                        startActivity(Intent.createChooser(intent, "Share via"));
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();

            return true;
        case R.id.action_share_app:
            try {
                Intent intent = new Intent(Intent.ACTION_SEND);
                intent.setType("text/plain");
                intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.app_name));
                String shareText = getString(R.string.app_description) + " Get it at http://tellmetime.info";
                intent.putExtra(Intent.EXTRA_TEXT, shareText);
                startActivity(Intent.createChooser(intent, "Share via"));
            } catch (Exception ignored) {
            }

            return true;
        case R.id.action_rate:
            Intent intent = new Intent(Intent.ACTION_VIEW);
            intent.setData(Uri.parse("market://details?id=info.tellmetime"));
            startActivity(intent);

            return true;
        default:
            return false;
        }
    }

    Clock mClockAlgorithm = new Clock(true) {

        /**
         * Function that needs to provide setting color of the given TextView.
         *
         * @param  item           String with the TextView's name.
         * @param  isHighlighting Indicates whether the function should highlight or dim the given item.
         */
        @Override
        @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
        protected void setColor(final String item, final boolean isHighlighting) {
            final int newColor = isHighlighting ? mHighlightColor : mBacklightColor;

            final int shadowSize = Math
                    .min((isHighlighting ? mShorterEdge / 100 : mShorterEdge / 200) * (int) mDensity, 25);

            // Since messing with UI is possible only on the main thread (i.e. UI thread)
            // and clock is ticking in separated thread, we need to set color in UI thread.
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    final TextView tv = (TextView) findViewById(getItem(item));

                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                        final Property<TextView, Integer> propertyHighlight = new Property<TextView, Integer>(
                                int.class, "textColor") {
                            @Override
                            public Integer get(TextView object) {
                                return object.getCurrentTextColor();
                            }

                            @Override
                            public void set(TextView object, Integer value) {
                                object.setTextColor(value);
                                object.setShadowLayer(shadowSize, 0, 0, value);
                            }
                        };

                        tv.setLayerType(View.LAYER_TYPE_HARDWARE, null);

                        ObjectAnimator animator = ObjectAnimator.ofInt(tv, propertyHighlight, newColor);
                        animator.setDuration(1500L);
                        animator.setEvaluator(new ArgbEvaluator());
                        animator.setInterpolator(new AccelerateDecelerateInterpolator());
                        animator.addListener(new AnimatorListenerAdapter() {
                            @Override
                            public void onAnimationEnd(Animator animation) {
                                tv.setLayerType(View.LAYER_TYPE_NONE, null);
                            }
                        });
                        animator.start();
                    } else {
                        tv.setTextColor(newColor);
                        tv.setShadowLayer(shadowSize, 0, 0, newColor);
                    }
                }
            });
        }

    };

    TimerTask mClockTask = new TimerTask() {

        @Override
        public void run() {
            mClockAlgorithm.tickTock();
        }

    };

    private void switchBackgroundMode(int position) {
        mBackgroundImage.setBackgroundResource(0);

        switch (position) {
        case MODE_BACKGROUND_SOLID:
            mBackgroundImage.setVisibility(View.GONE);
            break;

        case MODE_BACKGROUND_WALLPAPER:
            mBackgroundImage.setVisibility(View.VISIBLE);
            mBackgroundImage.setImageDrawable(WallpaperManager.getInstance(this).getDrawable());
            break;

        case MODE_BACKGROUND_IMAGE:
            if (mImage == null) {
                try {
                    File f = new File(getFilesDir(), IMAGE_FILE_NAME);
                    mImage = BitmapFactory.decodeStream(new FileInputStream(f));
                } catch (FileNotFoundException e) {
                    pickImage(null);
                }
            }

            mBackgroundImage.setVisibility(View.VISIBLE);
            mBackgroundImage.setImageBitmap(mImage);
            break;

        }
    }

    public void pickImage(View view) {
        Intent intent = new Intent();
        intent.setType("image/*");
        intent.setAction(Intent.ACTION_GET_CONTENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        startActivityForResult(intent, REQUEST_CODE);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
        if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            new Thread(new Runnable() {
                public void run() {
                    try {
                        InputStream stream = getContentResolver().openInputStream(data.getData());

                        // First decode with inJustDecodeBounds=true to check dimensions.
                        BitmapFactory.Options options = new BitmapFactory.Options();
                        options.inJustDecodeBounds = true;
                        BitmapFactory.decodeStream(stream, null, options);

                        // Calculate inSampleSize.

                        // Raw height and width of image.
                        final int height = options.outHeight;
                        final int width = options.outWidth;
                        int inSampleSize = 1;

                        if (height > mShorterEdge || width > mShorterEdge) {

                            final int halfHeight = height / 2;
                            final int halfWidth = width / 2;

                            // Calculate the largest inSampleSize value that is a power of 2 and keeps both
                            // height and width larger than the shorter screen edge.
                            while ((halfHeight / inSampleSize) > mShorterEdge
                                    && (halfWidth / inSampleSize) > mShorterEdge)
                                inSampleSize *= 2;
                        }

                        options.inSampleSize = inSampleSize;

                        // Decode bitmap with inSampleSize set.
                        stream = getContentResolver().openInputStream(data.getData());
                        options.inJustDecodeBounds = false;
                        mImage = BitmapFactory.decodeStream(stream, null, options);

                        mBackgroundImage.post(new Runnable() {
                            public void run() {
                                mBackgroundImage.setImageBitmap(mImage);
                            }
                        });

                        stream.close();

                        FileOutputStream outputStream = openFileOutput(IMAGE_FILE_NAME, Context.MODE_PRIVATE);
                        mImage.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
                        outputStream.close();
                    } catch (FileNotFoundException e) {
                        showToast(R.string.error_exception_file_not_found);
                        e.printStackTrace();
                    } catch (IOException e) {
                        showToast(R.string.error_exception_io);
                        e.printStackTrace();
                    } catch (SecurityException e) {
                        showToast(R.string.error_exception_security);
                        e.printStackTrace();
                    } catch (Exception e) {
                        showToast(R.string.error_exception);
                        e.printStackTrace();
                    }
                }
            }).start();
        }

        super.onActivityResult(requestCode, resultCode, data);
    }

    private void resizeClock() {
        final LinearLayout mClock = (LinearLayout) findViewById(R.id.clock);

        // Set width of #mClock layout to the screen's shorter edge size, so clock is not
        // expanded in landscape mode, but has rather somewhat a square shape.
        RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,
                FrameLayout.LayoutParams.WRAP_CONTENT);
        lp.addRule(RelativeLayout.CENTER_IN_PARENT);
        lp.width = mShorterEdge;
        mClock.setLayoutParams(lp);

        final float mItemSize = mShorterEdge / mClock.getChildCount();
        final int mRowMargin = (int) -(mItemSize / 2.2);

        // Scale text size according to shorter edge and set spacing between rows.
        for (int i = 0; i < mClock.getChildCount(); i++) {
            LinearLayout row = (LinearLayout) mClock.getChildAt(i);

            LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) row.getLayoutParams();
            params.bottomMargin = mRowMargin;
            row.setLayoutParams(params);

            for (int j = 0; j < row.getChildCount(); j++)
                ((TextView) row.getChildAt(j)).setTextSize(TypedValue.COMPLEX_UNIT_PX, mItemSize);
        }
        LinearLayout lastRow = (LinearLayout) mClock.getChildAt(mClock.getChildCount() - 1);
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) lastRow.getLayoutParams();
        params.bottomMargin = 0;
        lastRow.setLayoutParams(params);

        TextView twenty = (TextView) findViewById(R.id.twenty);
        params = (LinearLayout.LayoutParams) twenty.getLayoutParams();
        params.leftMargin = 0;
        twenty.setLayoutParams(params);

        // Inflates minutes indicators and attaches them to main view.
        FrameLayout minutesLayout = (FrameLayout) findViewById(R.id.minutes_indicators);
        minutesLayout.removeAllViews();
        LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        final boolean isLandscape = getResources()
                .getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
        inflater.inflate(isLandscape ? R.layout.minutes_land : R.layout.minutes_portrait, minutesLayout);
        RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
                FrameLayout.LayoutParams.MATCH_PARENT,
                isLandscape ? FrameLayout.LayoutParams.MATCH_PARENT : FrameLayout.LayoutParams.WRAP_CONTENT);
        if (!isLandscape) {
            layoutParams.addRule(RelativeLayout.BELOW, R.id.clock);
            layoutParams.topMargin = (int) -TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mMinutesSize / 3,
                    getResources().getDisplayMetrics());
        }
        minutesLayout.setLayoutParams(layoutParams);

        ViewGroup minutesDots = (ViewGroup) findViewById(R.id.minutes_dots);
        for (int i = 0; i < minutesDots.getChildCount(); i++)
            ((TextView) minutesDots.getChildAt(i)).setTextSize(TypedValue.COMPLEX_UNIT_DIP, mMinutesSize);
    }

    /**
     * Generates rainbow gradient and sets it as #mSeekBarHighlight progress drawable.
     */
    private void drawRainbow() {
        float gradientWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 256,
                getResources().getDisplayMetrics());
        int[] colors = { 0xFFFFFFFF, 0xFFFF0000, 0xFFFFFF00, 0xFF00FF00, 0xFF00FFFF, 0xFF0000FF, 0xFFFF00FF,
                0xFF888888, 0xFF000000 };
        LinearGradient rainbowGradient = new LinearGradient(0f, 0f, gradientWidth, 0f, colors, null,
                Shader.TileMode.CLAMP);
        float r = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics());
        ShapeDrawable shape = new ShapeDrawable(
                new RoundRectShape(new float[] { r, r, r, r, r, r, r, r }, null, null));
        shape.getPaint().setShader(rainbowGradient);
        mSeekBarHighlight.setProgressDrawable(shape);

        // Generate bitmap with the same rainbow gradient and cache it for later use to retrieve color at given position.
        Bitmap bitmap = Bitmap.createBitmap((int) gradientWidth, 1, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setStyle(Paint.Style.FILL);
        paint.setShader(rainbowGradient);
        canvas.drawRect(0, 0, gradientWidth, 1, paint);
        mRainbow = bitmap;

        // Set max value to gradient's width and restore relative position.
        mSeekBarHighlight.setMax((int) (gradientWidth - 1));
        mSeekBarHighlight.setProgress((int) ((gradientWidth - 1) * mHighlightPosition));
    }

    private void toggleMode() {
        isNightMode = !isNightMode;
        wasModeToggled = true; // Prevents toggling panels on ACTION_UP after long click.

        findViewById(R.id.touchZone).setBackgroundColor(
                getResources().getColor(isNightMode ? R.color.night_mode_overlay : android.R.color.transparent));

        mSettings.edit().putBoolean(BACKGROUND_MODE_TOGGLED, true).commit();
    }

    public void showToast(final int resID) {
        runOnUiThread(new Runnable() {
            public void run() {
                Toast.makeText(getApplicationContext(), resID, Toast.LENGTH_LONG).show();
            }
        });
    }

}