org.dmfs.tasks.ViewTaskFragment.java Source code

Java tutorial

Introduction

Here is the source code for org.dmfs.tasks.ViewTaskFragment.java

Source

/*
 * Copyright (C) 2013 Marten Gajda <marten@dmfs.org>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */

package org.dmfs.tasks;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import org.dmfs.android.retentionmagic.SupportFragment;
import org.dmfs.android.retentionmagic.annotations.Parameter;
import org.dmfs.android.retentionmagic.annotations.Retain;
import org.dmfs.provider.tasks.TaskContract.Tasks;
import org.dmfs.tasks.model.ContentSet;
import org.dmfs.tasks.model.Model;
import org.dmfs.tasks.model.OnContentChangeListener;
import org.dmfs.tasks.model.Sources;
import org.dmfs.tasks.model.TaskFieldAdapters;
import org.dmfs.tasks.notification.TaskNotificationHandler;
import org.dmfs.tasks.utils.ContentValueMapper;
import org.dmfs.tasks.utils.OnModelLoadedListener;
import org.dmfs.tasks.widget.ListenableScrollView;
import org.dmfs.tasks.widget.ListenableScrollView.OnScrollListener;
import org.dmfs.tasks.widget.TaskView;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.database.ContentObserver;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Build.VERSION;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.ActionBar;
import android.support.v7.app.ActionBarActivity;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.Toast;

/**
 * A fragment representing a single Task detail screen. This fragment is either contained in a {@link TaskListActivity} in two-pane mode (on tablets) or in a
 * {@link ViewTaskActivity} on handsets.
 * 
 * @author Arjun Naik <arjun@arjunnaik.in>
 * @author Marten Gajda <marten@dmfs.org>
 */
public class ViewTaskFragment extends SupportFragment implements OnModelLoadedListener, OnContentChangeListener {
    private final static String ARG_URI = "uri";

    /**
     * Edit action assigned to the floating action button.
     */
    private final static int ACTION_EDIT = 1;

    /**
     * Complete action assigned to the floating action button.
     */
    private final static int ACTION_COMPLETE = 2;

    /**
     * A set of values that may affect the recurrence set of a task. If one of these values changes we have to submit all of them.
     */
    private final static Set<String> RECURRENCE_VALUES = new HashSet<String>(Arrays.asList(new String[] { Tasks.DUE,
            Tasks.DTSTART, Tasks.TZ, Tasks.IS_ALLDAY, Tasks.RRULE, Tasks.RDATE, Tasks.EXDATE }));

    /**
     * The {@link ContentValueMapper} that knows how to map the values in a cursor to {@link ContentValues}.
     */
    private static final ContentValueMapper CONTENT_VALUE_MAPPER = EditTaskFragment.CONTENT_VALUE_MAPPER;

    /**
     * The {@link Uri} of the current task in the view.
     */
    @Parameter(key = ARG_URI)
    @Retain
    private Uri mTaskUri;

    /**
     * The values of the current task.
     */
    @Retain
    private ContentSet mContentSet;

    /**
     * The view that contains the details.
     */
    private ViewGroup mContent;

    /**
     * The {@link Model} of the current task.
     */
    private Model mModel;

    /**
     * The application context.
     */
    private Context mAppContext;

    /**
     * The actual detail view. We store this direct reference to be able to clear it when the fragment gets detached.
     */
    private TaskView mDetailView;

    private View mColorBar;
    private int mListColor;
    private View mActionButton;
    private ListenableScrollView mRootView;
    private int mOldStatus = -1;
    private boolean mIsPinned = false;
    private boolean mRestored;

    /**
     * The current action that's assigned to the floating action button.
     */
    private int mActionButtonAction = ACTION_COMPLETE;

    /**
     * A {@link Callback} to the activity.
     */
    private Callback mCallback;

    /**
     * A Runnable that updates the view.
     */
    private Runnable mUpdateViewRunnable = new Runnable() {
        @Override
        public void run() {
            updateView();
        }
    };

    public interface Callback {
        /**
         * This is called to instruct the Activity to call the editor for a specific task.
         * 
         * @param taskUri
         *            The {@link Uri} of the task to edit.
         * @param data
         *            The task data that belongs to the {@link Uri}. This is purely an optimization and may be <code>null</code>.
         */
        public void onEditTask(Uri taskUri, ContentSet data);

        /**
         * This is called to inform the Activity that a task has been deleted.
         * 
         * @param taskUri
         *            The {@link Uri} of the deleted task. Note that the Uri is likely to be invalid at the time of calling this method.
         */
        public void onDelete(Uri taskUri);

        /**
         * Notifies the listener about the list color of the current task.
         * 
         * @param color
         *            The color.
         */
        public void updateColor(int color);
    }

    public static ViewTaskFragment newInstance(Uri uri) {
        ViewTaskFragment result = new ViewTaskFragment();
        if (uri != null) {
            Bundle args = new Bundle();
            args.putParcelable(ARG_URI, uri);
            result.setArguments(args);
        }
        return result;
    }

    /**
     * Mandatory empty constructor for the fragment manager to instantiate the fragment (e.g. upon screen orientation changes).
     */
    public ViewTaskFragment() {
    }

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

        setHasOptionsMenu(true);
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);

        if (!(activity instanceof Callback)) {
            throw new IllegalStateException("Activity must implement TaskViewDetailFragment callback.");
        }

        mCallback = (Callback) activity;
        mAppContext = activity.getApplicationContext();
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        // remove listener
        if (mContentSet != null) {
            mContentSet.removeOnChangeListener(this, null);
        }

        if (mTaskUri != null) {
            mAppContext.getContentResolver().unregisterContentObserver(mObserver);
        }

        if (mContent != null) {
            mContent.removeAllViews();
        }

        if (mDetailView != null) {
            // remove values, to ensure all listeners get released
            mDetailView.setValues(null);
        }

    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        ListenableScrollView rootView = mRootView = (ListenableScrollView) inflater
                .inflate(R.layout.fragment_task_view_detail, container, false);
        mContent = (ViewGroup) rootView.findViewById(R.id.content);
        mColorBar = rootView.findViewById(R.id.headercolorbar);

        mRestored = savedInstanceState != null;

        if (savedInstanceState != null) {
            if (mContent != null && mContentSet != null) {
                // register for content updates
                mContentSet.addOnChangeListener(this, null, true);

                // register observer
                if (mTaskUri != null) {
                    mAppContext.getContentResolver().registerContentObserver(mTaskUri, false, mObserver);
                }
            }
        } else if (mTaskUri != null) {
            Uri uri = mTaskUri;
            // pretend we didn't load anything yet
            mTaskUri = null;
            loadUri(uri);
        }

        if (VERSION.SDK_INT >= 11 && mColorBar != null) {
            updateColor(0);
            mRootView.setOnScrollListener(new OnScrollListener() {

                @SuppressLint("NewApi")
                @Override
                public void onScroll(int oldScrollY, int newScrollY) {
                    int headerHeight = ((ActionBarActivity) getActivity()).getSupportActionBar().getHeight();
                    if (newScrollY <= headerHeight || oldScrollY <= headerHeight) {
                        updateColor((float) newScrollY / headerHeight);
                    }
                }
            });
        }

        return rootView;
    }

    @Override
    public void onPause() {
        super.onPause();
        persistTask();
    }

    private void persistTask() {
        Context activity = getActivity();
        if (mContentSet != null && mContentSet.isUpdate() && activity != null) {
            if (mContentSet.updatesAnyKey(RECURRENCE_VALUES)) {
                mContentSet.ensureUpdates(RECURRENCE_VALUES);
            }
            mContentSet.persist(activity);
        }
    }

    /**
     * Load the task with the given {@link Uri} in the detail view.
     * <p>
     * At present only Task Uris are supported.
     * </p>
     * TODO: add support for instance Uris.
     * 
     * @param uri
     *            The {@link Uri} of the task to show.
     */
    public void loadUri(Uri uri) {
        if (mTaskUri != null) {
            /*
             * Unregister the observer for any previously shown task first.
             */
            mAppContext.getContentResolver().unregisterContentObserver(mObserver);
            persistTask();
        }

        Uri oldUri = mTaskUri;
        mTaskUri = uri;
        if (uri != null) {
            /*
             * Create a new ContentSet and load the values for the given Uri. Also register listener and observer for changes in the ContentSet and the Uri.
             */
            mContentSet = new ContentSet(uri);
            mContentSet.addOnChangeListener(this, null, true);
            mAppContext.getContentResolver().registerContentObserver(uri, false, mObserver);
            mContentSet.update(mAppContext, CONTENT_VALUE_MAPPER);
        } else {
            /*
             * Immediately update the view with the empty task uri, i.e. clear the view.
             */
            mContentSet = null;
            if (mContent != null) {
                mContent.removeAllViews();
            }
        }

        if ((oldUri == null) != (uri == null)) {
            /*
             * getActivity().invalidateOptionsMenu() doesn't work in Android 2.x so use the compat lib
             */
            ActivityCompat.invalidateOptionsMenu(getActivity());
        }
    }

    /**
     * Update the detail view with the current ContentSet. This removes any previous detail view and creates a new one if {@link #mContentSet} is not
     * <code>null</code>.
     */
    private void updateView() {
        Activity activity = getActivity();
        if (mContent != null && activity != null) {
            final LayoutInflater inflater = (LayoutInflater) activity
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);

            if (mDetailView != null) {
                // remove values, to ensure all listeners get released
                mDetailView.setValues(null);
            }

            mContent.removeAllViews();
            if (mContentSet != null) {
                mDetailView = (TaskView) inflater.inflate(R.layout.task_view, mContent, false);
                mDetailView.setModel(mModel);
                mDetailView.setValues(mContentSet);
                mContent.addView(mDetailView);
            }

            mActionButton = mDetailView.findViewById(R.id.action_button);

            if (mActionButton != null) {
                mActionButton.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        switch (mActionButtonAction) {
                        case ACTION_COMPLETE: {
                            completeTask();
                            break;
                        }
                        case ACTION_EDIT: {
                            mCallback.onEditTask(mTaskUri, mContentSet);
                            break;
                        }
                        }
                    }
                });
            }

            if (mContentSet != null && TaskFieldAdapters.IS_CLOSED.get(mContentSet)) {
                ((ImageView) mActionButton.findViewById(android.R.id.icon))
                        .setImageResource(R.drawable.content_edit);
                mActionButtonAction = ACTION_EDIT;
            } else {
                mActionButtonAction = ACTION_COMPLETE;
            }

            if (mColorBar != null) {
                updateColor((float) mRootView.getScrollY() / mColorBar.getMeasuredHeight());
            }
        }
    }

    /**
     * Update the view. This doesn't call {@link #updateView()} right away, instead it posts it.
     */
    private void postUpdateView() {
        if (mContent != null) {
            mContent.post(mUpdateViewRunnable);
        }
    }

    @Override
    public void onModelLoaded(Model model) {
        if (model == null) {
            return;
        }

        // the model has been loaded, now update the view
        if (mModel == null || !mModel.equals(model)) {
            mModel = model;
            if (mRestored) {
                // The fragment has been restored from a saved state
                // We need to wait until all views are ready, otherwise the new data might get lost and all widgets show their default state (and no data).
                postUpdateView();
            } else {
                // This is the initial update. Just go ahead and update the view right away to ensure the activity comes up with a filled form.
                updateView();
            }
        }
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        /*
         * Don't show any options if we don't have a task to show.
         */
        if (mTaskUri != null) {
            inflater.inflate(R.menu.view_task_fragment_menu, menu);

            if (mContentSet != null) {
                Integer status = TaskFieldAdapters.STATUS.get(mContentSet);
                if (status != null) {
                    mOldStatus = status;
                }
                if (TaskFieldAdapters.IS_CLOSED.get(mContentSet)
                        || status != null && status == Tasks.STATUS_COMPLETED) {
                    // we disable the edit option, because the task is completed and the action button shows the edit option.
                    MenuItem item = menu.findItem(R.id.edit_task);
                    item.setEnabled(false);
                    item.setVisible(false);
                }

                // check pinned status
                if (TaskFieldAdapters.PINNED.get(mContentSet)) {
                    // we disable the edit option, because the task is completed and the action button shows the edit option.
                    MenuItem item = menu.findItem(R.id.pin_task);
                    item.setIcon(R.drawable.ic_pin_off_white_24dp);
                } else {
                    MenuItem item = menu.findItem(R.id.pin_task);
                    item.setIcon(R.drawable.ic_pin_white_24dp);
                }
            }
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int itemId = item.getItemId();
        if (itemId == R.id.edit_task) {
            // open editor for this task
            mCallback.onEditTask(mTaskUri, mContentSet);
            return true;
        } else if (itemId == R.id.delete_task) {
            new AlertDialog.Builder(getActivity()).setTitle(R.string.confirm_delete_title).setCancelable(true)
                    .setNegativeButton(android.R.string.cancel, new OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            // nothing to do here
                        }
                    }).setPositiveButton(android.R.string.ok, new OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            // TODO: remove the task in a background task
                            mContentSet.delete(mAppContext);
                            mCallback.onDelete(mTaskUri);
                        }
                    }).setMessage(R.string.confirm_delete_message).create().show();
            return true;
        } else if (itemId == R.id.complete_task) {
            completeTask();
            return true;
        } else if (itemId == R.id.pin_task) {
            if (TaskFieldAdapters.PINNED.get(mContentSet)) {
                item.setIcon(R.drawable.ic_pin_white_24dp);
                TaskNotificationHandler.unpinTask(mAppContext, mContentSet);
            } else {
                item.setIcon(R.drawable.ic_pin_off_white_24dp);
                TaskNotificationHandler.pinTask(mAppContext, mContentSet);
            }
            persistTask();
            return true;
        } else {
            return super.onOptionsItemSelected(item);
        }
    }

    /**
     * Completes the current task.
     */
    private void completeTask() {
        TaskFieldAdapters.STATUS.set(mContentSet, Tasks.STATUS_COMPLETED);
        TaskFieldAdapters.PINNED.set(mContentSet, false);
        persistTask();
        Toast.makeText(mAppContext,
                getString(R.string.toast_task_completed, TaskFieldAdapters.TITLE.get(mContentSet)),
                Toast.LENGTH_SHORT).show();
        // at present we just handle it like deletion, i.e. close the task in phone mode, do nothing in tablet mode
        mCallback.onDelete(mTaskUri);
    }

    public static int darkenColor(int color) {
        float[] hsv = new float[3];
        Color.colorToHSV(color, hsv);
        if (hsv[2] > 0.8) {
            hsv[2] = 0.8f + (hsv[2] - 0.8f) * 0.5f;
            color = Color.HSVToColor(hsv);
        }
        return color;
    }

    public static int darkenColor2(int color) {
        float[] hsv = new float[3];
        Color.colorToHSV(color, hsv);
        hsv[2] = hsv[2] * 0.75f;
        color = Color.HSVToColor(hsv);
        return color;
    }

    @SuppressLint("NewApi")
    private void updateColor(float percentage) {
        if (getActivity() instanceof TaskListActivity) {
            return;
        }

        float[] hsv = new float[3];
        Color.colorToHSV(mListColor, hsv);

        if (VERSION.SDK_INT >= 11 && mColorBar != null) {
            percentage = Math.max(0, Math.min(Float.isNaN(percentage) ? 0 : percentage, 1));
            percentage = (float) Math.pow(percentage, 1.5);

            int newColor = darkenColor2(mListColor);

            hsv[2] *= (0.5 + 0.25 * percentage);

            ActionBar actionBar = ((ActionBarActivity) getActivity()).getSupportActionBar();
            actionBar.setBackgroundDrawable(
                    new ColorDrawable((newColor & 0x00ffffff) | ((int) (percentage * 255) << 24)));

            // this is a workaround to ensure the new color is applied on all devices, some devices show a transparent ActionBar if we don't do that.
            actionBar.setDisplayShowTitleEnabled(true);
            actionBar.setDisplayShowTitleEnabled(false);

            mColorBar.setBackgroundColor(mListColor);

            if (VERSION.SDK_INT >= 21) {
                Window window = getActivity().getWindow();
                window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
                window.setStatusBarColor(newColor | 0xff000000);
            }
        }

        if (mActionButton != null) {
            // adjust color of action button
            if (hsv[0] > 70 && hsv[0] < 170 && hsv[2] < 0.62) {
                mActionButton.setBackgroundResource(R.drawable.bg_actionbutton_light);
            } else {
                mActionButton.setBackgroundResource(R.drawable.bg_actionbutton);
            }
        }
    }

    @SuppressLint("NewApi")
    @Override
    public void onContentLoaded(ContentSet contentSet) {
        if (contentSet.containsKey(Tasks.ACCOUNT_TYPE)) {
            mListColor = TaskFieldAdapters.LIST_COLOR.get(contentSet);
            ((Callback) getActivity()).updateColor(darkenColor2(mListColor));

            if (VERSION.SDK_INT >= 11) {
                updateColor((float) mRootView.getScrollY()
                        / ((ActionBarActivity) getActivity()).getSupportActionBar().getHeight());
            }

            Activity activity = getActivity();
            int newStatus = TaskFieldAdapters.STATUS.get(contentSet);
            boolean newPinned = TaskFieldAdapters.PINNED.get(contentSet);
            if (VERSION.SDK_INT >= 11 && activity != null
                    && (hasNewStatus(newStatus) || wasPinChanged(newPinned))) {
                // new need to update the options menu, because the status of the task has changed
                activity.invalidateOptionsMenu();
            }

            mIsPinned = newPinned;
            mOldStatus = newStatus;

            if (mModel == null
                    || !TextUtils.equals(mModel.getAccountType(), contentSet.getAsString(Tasks.ACCOUNT_TYPE))) {
                Sources.loadModelAsync(mAppContext, contentSet.getAsString(Tasks.ACCOUNT_TYPE), this);
            } else {
                // the model didn't change, just update the view
                postUpdateView();
            }
        }
    }

    /**
     * An observer for the tasks URI. It updates the task view whenever the URI changes.
     */
    private final ContentObserver mObserver = new ContentObserver(null) {
        @Override
        public void onChange(boolean selfChange) {
            if (mContentSet != null) {
                // reload the task
                mContentSet.update(mAppContext, CONTENT_VALUE_MAPPER);
            }
        }
    };

    @Override
    public void onContentChanged(ContentSet contentSet) {
    }

    private boolean hasNewStatus(int newStatus) {
        return (mOldStatus != -1 && mOldStatus != newStatus
                || mOldStatus == -1 && TaskFieldAdapters.IS_CLOSED.get(mContentSet));
    }

    private boolean wasPinChanged(boolean newPinned) {
        return !(mIsPinned == newPinned);
    }
}