id.ridon.keude.views.ManageReposActivity.java Source code

Java tutorial

Introduction

Here is the source code for id.ridon.keude.views.ManageReposActivity.java

Source

/*
 * Copyright (C) 2010-12  Ciaran Gultnieks, ciaran@ciarang.com
 * Copyright (C) 2009  Roberto Jacinto, roberto.jacinto@caixamagica.pt
 *
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

package id.ridon.keude.views;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.app.NavUtils;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.ActionBarActivity;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import id.ridon.keude.Keude;
import id.ridon.keude.KeudeApp;
import id.ridon.keude.Preferences;
import id.ridon.keude.ProgressListener;
import id.ridon.keude.R;
import id.ridon.keude.UpdateService;
import id.ridon.keude.compat.ClipboardCompat;
import id.ridon.keude.data.NewRepoConfig;
import id.ridon.keude.data.Repo;
import id.ridon.keude.data.RepoProvider;
import id.ridon.keude.net.MDnsHelper;
import id.ridon.keude.net.MDnsHelper.DiscoveredRepo;
import id.ridon.keude.net.MDnsHelper.RepoScanListAdapter;
import id.ridon.keude.views.fragments.RepoDetailsFragment;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Date;
import java.util.Locale;

import javax.jmdns.ServiceInfo;

public class ManageReposActivity extends ActionBarActivity {

    /**
     * If we have a new repo added, or the address of a repo has changed, then
     * we when we're finished, we'll set this boolean to true in the intent that
     * we finish with, to signify that we want the main list of apps updated.
     */
    public static final String REQUEST_UPDATE = "update";

    private RepoListFragment listFragment;
    private AlertDialog addRepoDialog;
    private static final String DEFAULT_NEW_REPO_TEXT = "https://";

    private enum PositiveAction {
        ADD_NEW, ENABLE, IGNORE
    }

    private PositiveAction positiveAction;

    private UpdateService.UpdateReceiver updateHandler = null;

    private static boolean changed = false;

    /**
     * True if activity started with an intent such as from QR code. False if
     * opened from, e.g. the main menu.
     */
    private boolean isImportingRepo = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        ((KeudeApp) getApplication()).applyTheme(this);
        super.onCreate(savedInstanceState);

        FragmentManager fm = getSupportFragmentManager();
        if (fm.findFragmentById(android.R.id.content) == null) {
            /*
             * Need to set a dummy view (which will get overridden by the
             * fragment manager below) so that we can call setContentView().
             * This is a work around for a (bug?) thing in 3.0, 3.1 which
             * requires setContentView to be invoked before the actionbar is
             * played with:
             * http://blog.perpetumdesign.com/2011/08/strange-case-of
             * -dr-action-and-mr-bar.html
             */
            setContentView(new LinearLayout(this));

            listFragment = new RepoListFragment();
            fm.beginTransaction().add(android.R.id.content, listFragment).commit();
        }

        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        // title is "Repositories" here, but "F-Droid" in VIEW Intent chooser
        getSupportActionBar().setTitle(R.string.menu_manage);
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (updateHandler != null) {
            updateHandler.showDialog(this);
        }
        /* let's see if someone is trying to send us a new repo */
        addRepoFromIntent(getIntent());
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (updateHandler != null) {
            updateHandler.hideDialog();
        }
    }

    @Override
    protected void onNewIntent(Intent intent) {
        setIntent(intent);
    }

    @Override
    public void finish() {
        Intent ret = new Intent();
        markChangedIfRequired(ret);
        setResult(Activity.RESULT_OK, ret);
        super.finish();
    }

    private boolean hasChanged() {
        return changed;
    }

    private void markChangedIfRequired(Intent intent) {
        if (hasChanged()) {
            Log.i("Keude", "Repo details have changed, prompting for update.");
            intent.putExtra(REQUEST_UPDATE, true);
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.manage_repos, menu);
        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case android.R.id.home:
            Intent destIntent = new Intent(this, Keude.class);
            markChangedIfRequired(destIntent);
            setResult(RESULT_OK, destIntent);
            NavUtils.navigateUpTo(this, destIntent);
            return true;
        case R.id.action_add_repo:
            showAddRepo();
            return true;
        case R.id.action_update_repo:
            updateRepos();
            return true;
        case R.id.action_find_local_repos:
            scanForRepos();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    private void updateRepos() {
        updateHandler = UpdateService.updateNow(this).setListener(new ProgressListener() {
            @Override
            public void onProgress(Event event) {
                if (event.type.equals(UpdateService.EVENT_COMPLETE_AND_SAME)
                        || event.type.equals(UpdateService.EVENT_COMPLETE_WITH_CHANGES)) {
                    // No need to prompt to update any more, we just
                    // did it!
                    changed = false;
                }

                if (event.type.equals(UpdateService.EVENT_FINISHED)) {
                    updateHandler = null;
                }
            }
        });
    }

    private void scanForRepos() {
        final RepoScanListAdapter adapter = new RepoScanListAdapter(this);
        final MDnsHelper mDnsHelper = new MDnsHelper(this, adapter);

        final View view = getLayoutInflater().inflate(R.layout.repodiscoverylist, null);
        final ListView repoScanList = (ListView) view.findViewById(R.id.reposcanlist);

        final AlertDialog alrt = new AlertDialog.Builder(this).setView(view)
                .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        mDnsHelper.stopDiscovery();
                        dialog.dismiss();
                    }
                }).create();

        alrt.setTitle(R.string.local_repos_title);
        alrt.setOnDismissListener(new DialogInterface.OnDismissListener() {
            @Override
            public void onDismiss(DialogInterface dialog) {
                mDnsHelper.stopDiscovery();
            }
        });

        repoScanList.setAdapter(adapter);
        repoScanList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, final View view, int position, long id) {

                final DiscoveredRepo discoveredService = (DiscoveredRepo) parent.getItemAtPosition(position);

                final ServiceInfo serviceInfo = discoveredService.getServiceInfo();
                String type = serviceInfo.getPropertyString("type");
                String protocol = type.contains("fdroidrepos") ? "https:/" : "http:/";
                String path = serviceInfo.getPropertyString("path");
                if (TextUtils.isEmpty(path))
                    path = "/fdroid/repo";
                String serviceUrl = protocol + serviceInfo.getInetAddresses()[0] + ":" + serviceInfo.getPort()
                        + path;
                showAddRepo(serviceUrl, serviceInfo.getPropertyString("fingerprint"));
            }
        });

        alrt.show();
        mDnsHelper.discoverServices();
    }

    public void importRepo(String uri, String fingerprint) {
        isImportingRepo = true;
        showAddRepo(uri, fingerprint);
    }

    private void showAddRepo() {
        /*
         * If there is text in the clipboard, and it looks like a URL, use that.
         * Otherwise use "https://" as default repo string.
         */
        ClipboardCompat clipboard = ClipboardCompat.create(this);
        String text = clipboard.getText();
        String fingerprint = null;
        if (!TextUtils.isEmpty(text)) {
            try {
                new URL(text);
                Uri uri = Uri.parse(text);
                fingerprint = uri.getQueryParameter("fingerprint");
                // uri might contain a QR-style, all uppercase URL:
                if (TextUtils.isEmpty(fingerprint))
                    fingerprint = uri.getQueryParameter("FINGERPRINT");
                text = NewRepoConfig.sanitizeRepoUri(uri);
            } catch (MalformedURLException e) {
                text = null;
            }
        }

        if (TextUtils.isEmpty(text)) {
            text = DEFAULT_NEW_REPO_TEXT;
        }
        showAddRepo(text, fingerprint);
    }

    private void showAddRepo(String newAddress, String newFingerprint) {
        final View view = getLayoutInflater().inflate(R.layout.addrepo, null);
        addRepoDialog = new AlertDialog.Builder(this).setView(view).create();
        final EditText uriEditText = (EditText) view.findViewById(R.id.edit_uri);
        final EditText fingerprintEditText = (EditText) view.findViewById(R.id.edit_fingerprint);

        /*
         * If the "add new repo" dialog is launched by an action outside of
         * Keude, i.e. a URL, then check to see if any existing repos match,
         * and change the action accordingly.
         */
        final Repo repo = (newAddress != null && isImportingRepo)
                ? RepoProvider.Helper.findByAddress(this, newAddress)
                : null;

        addRepoDialog.setIcon(android.R.drawable.ic_menu_add);
        addRepoDialog.setTitle(getString(R.string.repo_add_title));
        addRepoDialog.setButton(DialogInterface.BUTTON_POSITIVE, getString(R.string.repo_add_add),
                new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {

                        String fp = fingerprintEditText.getText().toString();

                        // the DB uses null for no fingerprint but the above
                        // code returns "" rather than null if its blank
                        if (fp.equals(""))
                            fp = null;

                        if (positiveAction == PositiveAction.ADD_NEW)
                            createNewRepo(uriEditText.getText().toString(), fp);
                        else if (positiveAction == PositiveAction.ENABLE)
                            createNewRepo(repo);
                    }
                });

        addRepoDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getString(R.string.cancel),
                new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                });
        addRepoDialog.show();

        final TextView overwriteMessage = (TextView) view.findViewById(R.id.overwrite_message);
        overwriteMessage.setVisibility(View.GONE);
        if (repo == null) {
            // no existing repo, add based on what we have
            positiveAction = PositiveAction.ADD_NEW;
        } else {
            // found the address in the DB of existing repos
            final Button addButton = addRepoDialog.getButton(DialogInterface.BUTTON_POSITIVE);
            addRepoDialog.setTitle(R.string.repo_exists);
            overwriteMessage.setVisibility(View.VISIBLE);
            if (newFingerprint != null)
                newFingerprint = newFingerprint.toUpperCase(Locale.ENGLISH);
            if (repo.fingerprint == null && newFingerprint != null) {
                // we're upgrading from unsigned to signed repo
                overwriteMessage.setText(R.string.repo_exists_add_fingerprint);
                addButton.setText(R.string.add_key);
                positiveAction = PositiveAction.ADD_NEW;
            } else if (newFingerprint == null || newFingerprint.equals(repo.fingerprint)) {
                // this entry already exists and is not enabled, offer to
                // enable
                // it
                if (repo.inuse) {
                    addRepoDialog.dismiss();
                    Toast.makeText(this, R.string.repo_exists_and_enabled, Toast.LENGTH_LONG).show();
                    return;
                } else {
                    overwriteMessage.setText(R.string.repo_exists_enable);
                    addButton.setText(R.string.enable);
                    positiveAction = PositiveAction.ENABLE;
                }
            } else {
                // same address with different fingerprint, this could be
                // malicious, so force the user to manually delete the repo
                // before adding this one
                overwriteMessage.setTextColor(getResources().getColor(R.color.red));
                overwriteMessage.setText(R.string.repo_delete_to_overwrite);
                addButton.setText(R.string.overwrite);
                addButton.setEnabled(false);
                positiveAction = PositiveAction.IGNORE;
            }
        }

        if (newFingerprint != null)
            fingerprintEditText.setText(newFingerprint);

        if (newAddress != null) {
            // This trick of emptying text then appending,
            // rather than just setting in the first place,
            // is neccesary to move the cursor to the end of the input.
            uriEditText.setText("");
            uriEditText.append(newAddress);
        }
    }

    /**
     * Adds a new repo to the database.
     */
    private void createNewRepo(String address, String fingerprint) {
        ContentValues values = new ContentValues(2);
        values.put(RepoProvider.DataColumns.ADDRESS, address);
        if (fingerprint != null && fingerprint.length() > 0) {
            values.put(RepoProvider.DataColumns.FINGERPRINT, fingerprint.toUpperCase(Locale.ENGLISH));
        }
        RepoProvider.Helper.insert(this, values);
        finishedAddingRepo();
    }

    /**
     * Seeing as this repo already exists, we will force it to be enabled again.
     */
    private void createNewRepo(Repo repo) {
        ContentValues values = new ContentValues(1);
        values.put(RepoProvider.DataColumns.IN_USE, 1);
        RepoProvider.Helper.update(this, repo, values);
        repo.inuse = true;
        finishedAddingRepo();
    }

    /**
     * If started by an intent that expects a result (e.g. QR codes) then we
     * will set a result and finish. Otherwise, we'll refresh the list of repos
     * to reflect the newly created repo.
     */
    private void finishedAddingRepo() {
        changed = true;
        addRepoDialog = null;
        if (isImportingRepo) {
            setResult(Activity.RESULT_OK);
            finish();
        }
    }

    private void addRepoFromIntent(Intent intent) {
        /* an URL from a click, NFC, QRCode scan, etc */
        NewRepoConfig newRepoConfig = new NewRepoConfig(this, intent);
        if (newRepoConfig.isValidRepo()) {
            importRepo(newRepoConfig.getUriString(), newRepoConfig.getFingerprint());
            checkIfNewRepoOnSameWifi(newRepoConfig);
        } else if (newRepoConfig.getErrorMessage() != null) {
            Toast.makeText(this, newRepoConfig.getErrorMessage(), Toast.LENGTH_LONG).show();
        }
    }

    private void checkIfNewRepoOnSameWifi(NewRepoConfig newRepo) {
        // if this is a local repo, check we're on the same wifi
        if (!TextUtils.isEmpty(newRepo.getBssid())) {
            WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
            WifiInfo wifiInfo = wifiManager.getConnectionInfo();
            String bssid = wifiInfo.getBSSID();
            if (TextUtils.isEmpty(bssid)) /* not all devices have wifi */
                return;
            bssid = bssid.toLowerCase(Locale.ENGLISH);
            String newRepoBssid = Uri.decode(newRepo.getBssid()).toLowerCase(Locale.ENGLISH);
            if (!bssid.equals(newRepoBssid)) {
                String msg = String.format(getString(R.string.not_on_same_wifi), newRepo.getSsid());
                Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
            }
            // TODO we should help the user to the right thing here,
            // instead of just showing a message!
        }
    }

    public static class RepoListFragment extends ListFragment
            implements LoaderManager.LoaderCallbacks<Cursor>, RepoAdapter.EnabledListener {

        @Override
        public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
            Uri uri = RepoProvider.getContentUri();
            Log.i("Keude", "Creating repo loader '" + uri + "'.");
            String[] projection = new String[] { RepoProvider.DataColumns._ID, RepoProvider.DataColumns.NAME,
                    RepoProvider.DataColumns.PUBLIC_KEY, RepoProvider.DataColumns.FINGERPRINT,
                    RepoProvider.DataColumns.IN_USE };
            return new CursorLoader(getActivity(), uri, projection, null, null, null);
        }

        @Override
        public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
            repoAdapter.swapCursor(cursor);
        }

        @Override
        public void onLoaderReset(Loader<Cursor> cursorLoader) {
            repoAdapter.swapCursor(null);
        }

        /**
         * NOTE: If somebody toggles a repo off then on again, it will have
         * removed all apps from the index when it was toggled off, so when it
         * is toggled on again, then it will require a refresh. Previously, I
         * toyed with the idea of remembering whether they had toggled on or
         * off, and then only actually performing the function when the activity
         * stopped, but I think that will be problematic. What about when they
         * press the home button, or edit a repos details? It will start to
         * become somewhat-random as to when the actual enabling, disabling is
         * performed. So now, it just does the disable as soon as the user
         * clicks "Off" and then removes the apps. To compensate for the removal
         * of apps from index, it notifies the user via a toast that the apps
         * have been removed. Also, as before, it will still prompt the user to
         * update the repos if you toggled on on.
         */
        @Override
        public void onSetEnabled(Repo repo, boolean isEnabled) {
            if (repo.inuse != isEnabled) {
                ContentValues values = new ContentValues(1);
                values.put(RepoProvider.DataColumns.IN_USE, isEnabled ? 1 : 0);
                RepoProvider.Helper.update(getActivity(), repo, values);

                if (isEnabled) {
                    changed = true;
                } else {
                    KeudeApp app = (KeudeApp) getActivity().getApplication();
                    RepoProvider.Helper.purgeApps(getActivity(), repo, app);
                    String notification = getString(R.string.repo_disabled_notification, repo.name);
                    Toast.makeText(getActivity(), notification, Toast.LENGTH_LONG).show();
                }
            }
        }

        private RepoAdapter repoAdapter;

        private View createHeaderView() {
            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
            TextView textLastUpdate = new TextView(getActivity());
            long lastUpdate = prefs.getLong(Preferences.PREF_UPD_LAST, 0);
            String lastUpdateCheck = "";
            if (lastUpdate == 0) {
                lastUpdateCheck = getString(R.string.never);
            } else {
                Date d = new Date(lastUpdate);
                lastUpdateCheck = DateFormat.getDateFormat(getActivity()).format(d) + " "
                        + DateFormat.getTimeFormat(getActivity()).format(d);
            }
            textLastUpdate.setText(getString(R.string.last_update_check, lastUpdateCheck));

            int sidePadding = (int) getResources().getDimension(R.dimen.padding_side);
            int topPadding = (int) getResources().getDimension(R.dimen.padding_top);

            textLastUpdate.setPadding(sidePadding, topPadding, sidePadding, topPadding);
            return textLastUpdate;
        }

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

            if (getListAdapter() == null) {
                /*
                 * Can't do this in the onCreate view, because "onCreateView"
                 * which returns the list view is "called between onCreate and
                 * onActivityCreated" according to the docs.
                 */
                getListView().addHeaderView(createHeaderView(), null, false);

                /*
                 * This could go in onCreate (and used to) but it needs to be
                 * called after addHeaderView, which can only be called after
                 * onCreate...
                 */
                repoAdapter = new RepoAdapter(getActivity(), null);
                repoAdapter.setEnabledListener(this);
                setListAdapter(repoAdapter);
            }
        }

        @Override
        public void onCreate(Bundle savedInstanceState) {

            super.onCreate(savedInstanceState);
            setRetainInstance(true);
            setHasOptionsMenu(true);
        }

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

            // Starts a new or restarts an existing Loader in this manager
            getLoaderManager().restartLoader(0, null, this);
        }

        @Override
        public void onListItemClick(ListView l, View v, int position, long id) {

            super.onListItemClick(l, v, position, id);

            Repo repo = new Repo((Cursor) getListView().getItemAtPosition(position));
            editRepo(repo);
        }

        public static final int SHOW_REPO_DETAILS = 1;

        public void editRepo(Repo repo) {
            Intent intent = new Intent(getActivity(), RepoDetailsActivity.class);
            intent.putExtra(RepoDetailsFragment.ARG_REPO_ID, repo.getId());
            startActivityForResult(intent, SHOW_REPO_DETAILS);
        }
    }
}