cz.maresmar.sfm.view.credential.CredentialDetailFragment.java Source code

Java tutorial

Introduction

Here is the source code for cz.maresmar.sfm.view.credential.CredentialDetailFragment.java

Source

/*
 * SmartFoodMenu - Android application for canteens extendable with plugins
 *
 * Copyright  2016-2018  Martin Mare <mmrmartin[at]gmail[dot]com>
 *
 * This program 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.
 *
 * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package cz.maresmar.sfm.view.credential;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import android.support.design.widget.Snackbar;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v7.widget.AppCompatCheckBox;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.SimpleCursorAdapter;
import android.widget.Spinner;

import java.util.concurrent.CountDownLatch;

import cz.maresmar.sfm.Assert;
import cz.maresmar.sfm.BuildConfig;
import cz.maresmar.sfm.R;
import cz.maresmar.sfm.plugin.ActionContract;
import cz.maresmar.sfm.provider.ProviderContract;
import cz.maresmar.sfm.view.DataForm;
import cz.maresmar.sfm.view.WithExtraFragment;
import timber.log.Timber;

/**
 * Fragment that shows one credential and allows to edit it.
 */
public class CredentialDetailFragment extends WithExtraFragment
        implements LoaderManager.LoaderCallbacks<Cursor>, DataForm {

    private static final int CREDENTIAL_LOADER_ID = 1;
    private static final int CREDENTIAL_GROUPS_LOADER_ID = 2;
    private static final int PORTAL_LOADER_ID = 3;

    private static final String ARG_START_WITH_EMPTY = "startWithEmpty";
    private static final String ARG_PORTAL_URI = "portalUri";
    private static final String ARG_CREDENTIAL_URI = "credentialUri";
    private static final String ARG_CREDENTIAL_TEMP_URI = "credentialTempUri";
    private static final String ARG_USER_PREFIX_URI = "userPrefixUri";
    private static final String ARG_PORTAL_GROUP_ID = "portalGroupId";
    private static final String ARG_LAST_PLUGIN = "lastPlugin";
    private static final String ARG_QUERY_URI = "queryUri";
    private static final String ARG_QUERY_SELECTION = "querySelection";

    // Local variables
    private Uri mCredentialUri;
    private Uri mCredentialTempUri;
    private Uri mUserPrefixUri;
    private Uri mPortalUri;
    private long mPortalGroupId;
    private String mLastPlugin = null;
    private boolean mLoadDataFromDb = true;

    // UI elements
    private EditText mNameText;
    private EditText mPasswordText;
    // Credential group
    private SimpleCursorAdapter mCredentialGroupAdapter;
    private Spinner mCredentialGroupSpinner;
    // Credit change notification
    private AppCompatCheckBox mCreditIncreaseNotificationCheckBox;
    private AppCompatCheckBox mLowCreditNotificationCheckBox;
    private EditText mLowCreditText;

    private CountDownLatch blockingLoaders = new CountDownLatch(1);

    /**
     * Create new fragment
     */
    public CredentialDetailFragment() {
        super(R.id.extras, ActionContract.FORMAT_TYPE_CREDENTIAL);
        // Required empty public constructor
    }

    /**
     * Create new empty credential fragment with default data
     * @param userPrefixUri User prefix Uri
     * @param portalUri Uri of one portal used with this credentials
     * @return New empty fragment
     */
    public static CredentialDetailFragment newEmptyInstance(@Nullable Uri userPrefixUri, @Nullable Uri portalUri) {
        CredentialDetailFragment fragment = new CredentialDetailFragment();
        Bundle args = new Bundle();
        args.putBoolean(ARG_START_WITH_EMPTY, true);
        args.putParcelable(ARG_PORTAL_URI, portalUri);
        args.putParcelable(ARG_USER_PREFIX_URI, userPrefixUri);
        fragment.setArguments(args);
        return fragment;
    }

    /**
     * Create credential from saved data
     * @param credentialUri Uri of saved credential
     * @return New fragment with saved data
     */
    public static CredentialDetailFragment newInstance(@NonNull Uri credentialUri) {
        CredentialDetailFragment fragment = new CredentialDetailFragment();
        Bundle args = new Bundle();
        args.putBoolean(ARG_START_WITH_EMPTY, false);
        args.putParcelable(ARG_CREDENTIAL_URI, credentialUri);
        fragment.setArguments(args);
        return fragment;
    }

    // -------------------------------------------------------------------------------------------
    // Lifecycle events
    // -------------------------------------------------------------------------------------------

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

        if (getArguments() != null) {
            if (getArguments().getBoolean(ARG_START_WITH_EMPTY)) {
                mUserPrefixUri = getArguments().getParcelable(ARG_USER_PREFIX_URI);
                mPortalUri = getArguments().getParcelable(ARG_PORTAL_URI);
            } else {
                mCredentialUri = getArguments().getParcelable(ARG_CREDENTIAL_URI);

                getLoaderManager().initLoader(CREDENTIAL_LOADER_ID, null, this);
            }
        }

        if (savedInstanceState != null) {
            mCredentialUri = savedInstanceState.getParcelable(ARG_CREDENTIAL_URI);
            mCredentialTempUri = savedInstanceState.getParcelable(ARG_CREDENTIAL_TEMP_URI);
            mPortalUri = savedInstanceState.getParcelable(ARG_PORTAL_URI);
            mUserPrefixUri = savedInstanceState.getParcelable(ARG_USER_PREFIX_URI);
            mPortalGroupId = savedInstanceState.getLong(ARG_PORTAL_GROUP_ID);
            mLastPlugin = savedInstanceState.getString(ARG_LAST_PLUGIN);
            mLoadDataFromDb = false;
        }
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View view = inflater.inflate(R.layout.fragment_credential_detail, container, false);

        mNameText = view.findViewById(R.id.nameText);
        mPasswordText = view.findViewById(R.id.passwordText);
        mCredentialGroupSpinner = view.findViewById(R.id.credentialGroupSpinner);

        mCreditIncreaseNotificationCheckBox = view.findViewById(R.id.increaseCheckBox);
        mLowCreditNotificationCheckBox = view.findViewById(R.id.lowCreditCheckBox);
        mLowCreditText = view.findViewById(R.id.lowCreditText);
        mLowCreditNotificationCheckBox
                .setOnCheckedChangeListener((buttonView, checked) -> mLowCreditText.setEnabled(checked));

        return view;
    }

    @Override
    public void onStart() {
        super.onStart();

        if (mPortalUri != null) {
            Bundle args = new Bundle();
            args.putParcelable(ARG_QUERY_URI, mPortalUri);
            args.putString(ARG_QUERY_SELECTION, null);

            getLoaderManager().initLoader(PORTAL_LOADER_ID, args, this);
        }

        getLoaderManager().initLoader(CREDENTIAL_GROUPS_LOADER_ID, null, this);
    }

    // -------------------------------------------------------------------------------------------
    // UI save and restore
    // -------------------------------------------------------------------------------------------

    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putParcelable(ARG_CREDENTIAL_URI, mCredentialUri);
        outState.putParcelable(ARG_CREDENTIAL_TEMP_URI, mCredentialTempUri);
        outState.putParcelable(ARG_PORTAL_URI, mPortalUri);
        outState.putParcelable(ARG_USER_PREFIX_URI, mUserPrefixUri);
        outState.putLong(ARG_PORTAL_GROUP_ID, mPortalGroupId);
        outState.putString(ARG_LAST_PLUGIN, mLastPlugin);
    }

    // -------------------------------------------------------------------------------------------
    // Data form manipulating methods
    // -------------------------------------------------------------------------------------------

    /**
     * Resets fragment to its default values
     * @param userPrefixUri User prefix uri
     * @param portalUri Uri of one portal used with this credentials
     */
    public void reset(@Nullable Uri userPrefixUri, @Nullable Uri portalUri) {
        // Delete old temp data
        discardTempData(getContext());

        // Loads new data
        mUserPrefixUri = userPrefixUri;
        mPortalUri = portalUri;
        if (mPortalUri != null) {
            Bundle args = new Bundle();
            args.putParcelable(ARG_QUERY_URI, mPortalUri);
            args.putString(ARG_QUERY_SELECTION, null);

            getLoaderManager().restartLoader(PORTAL_LOADER_ID, args, this);
        } else {
            mNameText.setText("");
            mPasswordText.setText("");
            setExtraData(null);
        }
    }

    // -------------------------------------------------------------------------------------------
    // Callbacks
    // -------------------------------------------------------------------------------------------

    @NonNull
    @Override
    public Loader<Cursor> onCreateLoader(int id, @Nullable Bundle args) {
        //noinspection ConstantConditions
        @NonNull
        Context context = getContext();
        switch (id) {
        case CREDENTIAL_LOADER_ID:
            return new CursorLoader(context, mCredentialUri,
                    new String[] { ProviderContract.Credentials.USER_NAME, ProviderContract.Credentials.USER_PASS,
                            ProviderContract.Credentials.CREDENTIALS_GROUP_ID, ProviderContract.Credentials.EXTRA,
                            ProviderContract.Credentials.PORTAL_GROUP_ID, ProviderContract.Credentials.FLAGS,
                            ProviderContract.Credentials.LOW_CREDIT_THRESHOLD },
                    null, null, null);
        case CREDENTIAL_GROUPS_LOADER_ID:
            return new CursorLoader(context, ProviderContract.CredentialsGroup.getUri(),
                    new String[] { ProviderContract.CredentialsGroup._ID, ProviderContract.CredentialsGroup.NAME },
                    null, null, null);
        case PORTAL_LOADER_ID:
            return new CursorLoader(context, args.getParcelable(ARG_QUERY_URI),
                    new String[] { ProviderContract.Portal.PLUGIN, ProviderContract.Portal.PORTAL_GROUP_ID, },
                    args.getString(ARG_QUERY_SELECTION), null, null);
        default:
            throw new UnsupportedOperationException("Unknown loader id: " + id);
        }
    }

    @Override
    public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
        switch (loader.getId()) {
        case CREDENTIAL_LOADER_ID:
            Timber.d("Credential data loaded");

            cursor.moveToFirst();
            if (BuildConfig.DEBUG) {
                Assert.isOne(cursor.getCount());
            }

            if (mPortalUri == null) {
                mPortalGroupId = cursor.getLong(4);

                String selection = ProviderContract.Portal.PORTAL_GROUP_ID + " = " + mPortalGroupId;
                Uri uri = ProviderContract.Portal.getUri();

                Bundle args = new Bundle();
                args.putParcelable(ARG_QUERY_URI, uri);
                args.putString(ARG_QUERY_SELECTION, selection);

                getLoaderManager().initLoader(PORTAL_LOADER_ID, args, this);
            }

            if (mLoadDataFromDb) {
                // Do not override changes on screen rotation
                mNameText.setText(cursor.getString(0));
                mPasswordText.setText(cursor.getString(1));
                long credentialGroupId = cursor.getLong(2);
                setExtraData(cursor.getString(3));

                @ProviderContract.CredentialFlags
                int flags = cursor.getInt(5);
                boolean increaseNotification = !((flags
                        & ProviderContract.CREDENTIAL_FLAG_DISABLE_CREDIT_INCREASE_NOTIFICATION) == ProviderContract.CREDENTIAL_FLAG_DISABLE_CREDIT_INCREASE_NOTIFICATION);
                mCreditIncreaseNotificationCheckBox.setChecked(increaseNotification);
                boolean lowCreditNotification = !((flags
                        & ProviderContract.CREDENTIAL_FLAG_DISABLE_LOW_CREDIT_NOTIFICATION) == ProviderContract.CREDENTIAL_FLAG_DISABLE_LOW_CREDIT_NOTIFICATION);
                mLowCreditNotificationCheckBox.setChecked(lowCreditNotification);
                mLowCreditText.setText(String.valueOf(cursor.getInt(6)));

                // Set credential group Id
                AsyncTask.execute(() -> {
                    try {
                        blockingLoaders.await();
                    } catch (InterruptedException e) {
                        Timber.e(e);
                    }
                    getActivity().runOnUiThread(() -> setCredentialGroupId(credentialGroupId));
                });
                mLoadDataFromDb = false;
            }
            break;
        case CREDENTIAL_GROUPS_LOADER_ID:
            Timber.d("Credential group data loaded");

            mCredentialGroupAdapter = new SimpleCursorAdapter(getContext(),
                    R.layout.support_simple_spinner_dropdown_item, cursor,
                    new String[] { ProviderContract.CredentialsGroup.NAME }, new int[] { android.R.id.text1 }, 0);
            mCredentialGroupSpinner.setAdapter(mCredentialGroupAdapter);
            blockingLoaders.countDown();
            break;
        case PORTAL_LOADER_ID:
            Timber.d("Portal data loaded");

            cursor.moveToFirst();
            if (BuildConfig.DEBUG) {
                Assert.that(cursor.getCount() > 0, "You should have at least one portal," + "but has %d",
                        cursor.getCount());
            }

            String pluginId = cursor.getString(0);
            requestExtraFormat(pluginId);
            if (mLastPlugin != null && !mLastPlugin.equals(pluginId)) {
                setExtraData(null);
            }
            mLastPlugin = pluginId;

            mPortalGroupId = cursor.getLong(1);
            break;
        default:
            throw new UnsupportedOperationException("Unknown loader id: " + loader.getId());
        }
    }

    @Override
    public void onLoaderReset(@NonNull Loader<Cursor> loader) {
        switch (loader.getId()) {
        case CREDENTIAL_LOADER_ID:
            Timber.e("Credential data %s is no longer valid", mCredentialUri);
            // Let's tread current user data as new entry
            reset(null, null);
            break;
        case CREDENTIAL_GROUPS_LOADER_ID:
            Timber.w("Credentials group loader reset called");
            mCredentialGroupAdapter.swapCursor(null);
            break;
        case PORTAL_LOADER_ID:
            Timber.w("Portal loader reset called");
            break;
        default:
            throw new UnsupportedOperationException("Unknown loader id: " + loader.getId());
        }
    }

    // -------------------------------------------------------------------------------------------
    // UI handling
    // -------------------------------------------------------------------------------------------

    private void setCredentialGroupId(long credentialGroupId) {
        // Parse selected id
        int position = -1;
        // Find position for id
        // There are only few entries in adapter so its ok to do it with for cycle
        for (int i = 0; i < mCredentialGroupAdapter.getCount(); i++) {
            if (mCredentialGroupAdapter.getItemId(i) == credentialGroupId) {
                position = i;
            }
        }
        if (BuildConfig.DEBUG) {
            Assert.that(position >= 0, "Illegal credential group id value %d", credentialGroupId);
        }
        // Set it in UI
        mCredentialGroupSpinner.setSelection(position);
    }

    // -------------------------------------------------------------------------------------------
    // Data form events
    // -------------------------------------------------------------------------------------------

    @Nullable
    @Override
    public Uri saveData() {
        Timber.i("Saving credential data");

        // Defines an object to contain the new values to insert
        ContentValues values = new ContentValues();

        /*
         * Sets the values of each column and inserts the word. The arguments to the "put"
         * method are "column name" and "value"
         */
        values.put(ProviderContract.Credentials.CREDENTIALS_GROUP_ID, mCredentialGroupSpinner.getSelectedItemId());
        values.put(ProviderContract.Credentials.PORTAL_GROUP_ID, mPortalGroupId);
        // User will be added from user prefix
        values.put(ProviderContract.Credentials.USER_NAME, mNameText.getText().toString());
        values.put(ProviderContract.Credentials.USER_PASS, mPasswordText.getText().toString());
        values.put(ProviderContract.Credentials.EXTRA, getExtraData());
        // Notifications
        @ProviderContract.CredentialFlags
        int flags = 0;
        if (!mCreditIncreaseNotificationCheckBox.isChecked()) {
            flags |= ProviderContract.CREDENTIAL_FLAG_DISABLE_CREDIT_INCREASE_NOTIFICATION;
        }
        if (!mLowCreditNotificationCheckBox.isChecked()) {
            flags |= ProviderContract.CREDENTIAL_FLAG_DISABLE_LOW_CREDIT_NOTIFICATION;
        }
        values.put(ProviderContract.Credentials.FLAGS, flags);
        values.put(ProviderContract.Credentials.LOW_CREDIT_THRESHOLD, mLowCreditText.getText().toString());

        //noinspection ConstantConditions
        ContentResolver contentResolver = getContext().getContentResolver();
        if (mCredentialUri == null) {
            Uri credentialUri = Uri.withAppendedPath(mUserPrefixUri, ProviderContract.CREDENTIALS_PATH);
            mCredentialTempUri = contentResolver.insert(credentialUri, values);
            mCredentialUri = mCredentialTempUri;
        } else {
            int updatedRows = contentResolver.update(mCredentialUri, values, null, null);
            if (BuildConfig.DEBUG) {
                Assert.isOne(updatedRows);
            }
        }
        return mCredentialUri;
    }

    @WorkerThread
    @Override
    public void discardTempData(@NonNull Context context) {
        Timber.i("Discarding credential data");

        if (mCredentialTempUri != null) {
            // Disable using of temp data
            mCredentialUri = null;

            // Delete credential temp data
            int affectedRows = context.getContentResolver().delete(mCredentialTempUri, null, null);
            if (BuildConfig.DEBUG) {
                Assert.isOne(affectedRows);
            }
            mCredentialTempUri = null;
        }
    }

    @Override
    public boolean hasValidData() {
        boolean isValid = true;

        if (!hasValidExtraData()) {
            isValid = false;
        }

        if (!isValid) {
            //noinspection ConstantConditions
            Snackbar.make(getView(), R.string.extra_invalid_input_error, Snackbar.LENGTH_LONG)
                    .setAction(android.R.string.ok, view -> {
                        // Only dismiss message
                    }).show();
        }

        return isValid;
    }
}