io.v.moments.ux.MainActivity.java Source code

Java tutorial

Introduction

Here is the source code for io.v.moments.ux.MainActivity.java

Source

// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package io.v.moments.ux;

import android.Manifest;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.MediaStore;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SwitchCompat;
import android.support.v7.widget.Toolbar;
import android.text.InputType;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;

import com.google.common.util.concurrent.FutureCallback;

import org.joda.time.Duration;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import io.v.moments.R;
import io.v.moments.ifc.Moment;
import io.v.moments.ifc.MomentFactory;
import io.v.moments.lib.DiscoveredList;
import io.v.moments.lib.FileUtil;
import io.v.moments.lib.Id;
import io.v.moments.lib.ObservedList;
import io.v.moments.lib.PermissionManager;
import io.v.moments.model.AdConverterMoment;
import io.v.moments.model.AdvertiserFactory;
import io.v.moments.model.BitMapper;
import io.v.moments.model.Config;
import io.v.moments.model.MomentAdCampaign;
import io.v.moments.model.MomentFactoryImpl;
import io.v.moments.model.StateStore;
import io.v.moments.model.Toaster;
import io.v.moments.v23.ifc.Advertiser;
import io.v.moments.v23.ifc.Scanner;
import io.v.moments.v23.ifc.V23Manager;
import io.v.moments.v23.impl.V23ManagerImpl;
import io.v.v23.security.Blessings;

/**
 * This app allows the user to take photos and advertise them on the network.
 * Other instances of the app will scan for and display such photos.
 *
 * This app is an example of running, advertising and scanning for multiple
 * services.
 *
 * A photo and its ancillary data (id, author, caption, date, ordinal number,
 * etc.) are called a Moment.  There are _local_ moments, created locally, and
 * _remote_ moments, found via discovery.  Local moments can be advertised so
 * that remote devices can discover and request them.  Remote moments are not
 * re-advertised.
 *
 * Local moments are persistent, in that the user must delete them to get rid of
 * them.  Remote moments are pulled in over the network, and not officially
 * retained between runs, though remote photo data may be left in local storage
 * as a simple cache. The moment's id is used to invalidate the cache.
 *
 * Every local moment is served by its own service.  This egregious use of
 * services allows for easy creation of multiple scan targets to exercise
 * discovery code. The involvement of a photo encourages the use of an RPC since
 * adding a (very large) photo to an advertisement as an attribute noticeably
 * increases the time taken between clicking 'advertise' on one device and
 * seeing any evidence of the advertisement on another device.
 *
 * TODO: when reloading from prefs, don't change advertise or scan state. only
 * do that when reloading from bundle. TODO: ScannerImpl should handle the
 * Update parsing currently done by DiscoveredList. TODO: unit tests. TODO: Add
 * version number to prefs, ignore and overwrite state if old version (to avoid
 * need to manually wipe data to avoid crashes).
 */
public class MainActivity extends AppCompatActivity implements SensorEventListener {
    private static final String TAG = "MainActivity";
    // Android Marshmallow permissions list.
    private static final String[] PERMS = { Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.BLUETOOTH_ADMIN, };
    // A string that unambiguously identifies the phone in human readable form.
    private static final String DEVICE_SIGNATURE = Build.MODEL + " " + Build.SERIAL;
    // For Marshmallow permissions.
    private final PermissionManager mPermissionManager = new PermissionManager(this, RequestCode.PERMISSIONS,
            PERMS);
    // Use a serial executor to assure serial execution, e.g. toggling a switch
    // on and off without a race condition.
    private final ExecutorService mSerialExecutor = Executors.newSingleThreadExecutor();
    // Use a pool when order isn't important.
    private final ExecutorService mPoolExecutor = Executors.newCachedThreadPool();
    // For changes to UX.
    private final Handler mHandler = new Handler(Looper.getMainLooper());
    // For discovery, serving and behaving as a client.
    private final V23Manager mV23Manager = V23ManagerImpl.Singleton.get();

    // See wireUxToDataModel for discussion of the following.
    private StateStore mStateStore;
    private AdvertiserFactory mAdvertiserFactory;
    private Scanner mScanner;
    private ScanSwitchHolder mScanSwitchHolder;
    private MomentFactory mMomentFactory;
    private BitMapper mBitMapper;
    private ObservedList<Moment> mLocalMoments;
    private DiscoveredList<Moment> mRemoteMoments;
    private Id mCurrentPhotoId;
    private boolean mShouldBeScanning = false;

    // When the device is shaken while this activity is in the foreground, a dialog pops
    // up prompting the user for an email address to which it will send an email detailing
    // how the recipient can inspect the state of the application. This may be useful for live
    // debugging (the recipient can inspect logs, exported stats, system information etc.).
    // Arguably, using a "shake" to initiate this interaction isn't particularly well thought out
    // UI, but we just wanted to demonstrate this remote inspection technique in this sample
    // application.
    private static final double SHAKE_THRESHOLD = 3;
    private static final int SHAKE_EVENT_MS = 500;
    private SensorManager mSensorManager;
    private Sensor mAccelerometer;
    private long mShakeTimestamp;
    private boolean mRemoteInspectionEnabled;

    /**
     * The number used in a moment's file name is called the moments 'ordinal'
     * number.  At the time of writing local moments are never individually
     * deleted - either they are all kept or all deleted when app data is
     * deleted. Therefore, the _next_ local ordinal is just an array size
     * increment.
     *
     * Remote (discovered) moments, however, come and go individually, so one
     * cannot use, say, the size of the current list of remote moments to
     * determine the next ordinal number, as one might use a number already in
     * use, and display the wrong photo.
     *
     * When the phone is rotated, discovery scanning stops, then restarts.  So
     * known moments are immediately 're-'discovered.  To avoid reacquiring
     * data, retain and use a cache of known remote moments.  When a discovery
     * is made, check the map, and if there's a hit, there's no need to contact
     * the remote service.  The cache can also be used to compute the 'next'
     * ordinal number for remote services.
     */
    private Map<Id, Moment> mRemoteMomentCache;

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

        setContentView(R.layout.activity_main);

        // This will look in prefs for a Vanadium blessing, and if not
        // found will leave this activity (onStop likely to be called) and
        // return via a start intent (not via onActivityResult).
        mV23Manager.init(this, onBlessings());

        wireUxToDataModel();
        initializeOrRestore(savedInstanceState);
        initializeShakeDetector();
    }

    private void initializeShakeDetector() {
        mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
        mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
    }

    private FutureCallback<Blessings> onBlessings() {
        return new FutureCallback<Blessings>() {
            @Override
            public void onSuccess(Blessings b) {
                Log.d(TAG, "Got blessings!");
                if (!mPermissionManager.haveAllPermissions()) {
                    Log.d(TAG, "Obtaining permissions");
                    mPermissionManager.obtainPermission();
                }
            }

            @Override
            public void onFailure(Throwable t) {
                Log.d(TAG, "Failure to get blessings, nothing will work.", t);
            }
        };
    }

    /**
     * This method builds the app's object graph, and intentionally has no
     * branches that depend on state loaded from the app's prefs or instance
     * bundle. State is loaded after the wiring is complete, to make phone
     * rotation easy.
     */
    private void wireUxToDataModel() {
        // Stores remote moments by Id to avoid having to wait for re-discovery.
        mRemoteMomentCache = new HashMap<>();

        // Compresses byte data, converts byte[] to bitmap, manages file storage.
        mBitMapper = Config.makeBitmapper(this);

        // Makes moments.  Each moment needs a bitmapper to read its BitMaps.
        mMomentFactory = new MomentFactoryImpl(mBitMapper);

        // Makes advertisers.  Needs v23Manager to do advertising.
        mAdvertiserFactory = new AdvertiserFactory(mV23Manager, mMomentFactory);

        // Local moments, with photos taken by the local device.
        mLocalMoments = new ObservedList<>();

        // Converts advertisements to 'remote' moments.  Needs v23Manager to
        // make RPCs, needs mMomentFactory to make moments, needs a thread pool
        // for making RPCs to get photos, needs mHandler to post photos on the
        // UX thread, fills the cache with remote moments.
        AdConverterMoment converter = new AdConverterMoment(mV23Manager, mMomentFactory, mPoolExecutor, mHandler,
                mRemoteMomentCache);

        // The list of remote (discovered) moments.  Pass mAdvertiserFactory
        // as a container of Id's to reject from discovery (because they
        // represent local moments).
        mRemoteMoments = new DiscoveredList<>(converter, mAdvertiserFactory, mHandler);

        Toaster toaster = new Toaster(this);

        mScanner = mV23Manager.makeScanner(MomentAdCampaign.QUERY);
        mScanSwitchHolder = new ScanSwitchHolder(toaster, mScanner, mRemoteMoments);

        // Stores app state to bundles, preferences, etc.  The mMomentFactory
        // needed to recreate moments.
        mStateStore = new StateStore(getSharedPreferences(nameOfSharedPrefs(), Context.MODE_PRIVATE),
                mMomentFactory);

        // Tell the converter where to place remote moments.
        converter.setList(mRemoteMoments);

        // The adapter allows remote and local moment lists to 'stack' in a
        // RecyclerView.  mAdvertiserFactory is used to generate advertisers
        // for local moments when a user wants to advertise them.  The
        // serialExecutor is used to start/stop advertisements in the UX.
        MomentAdapter adapter = new MomentAdapter(mRemoteMoments, mLocalMoments, toaster, mAdvertiserFactory);

        // Lets the adapter speed up a bit.
        adapter.setHasStableIds(true);

        // Hook the adapter to a view, and begin observing changes to the
        // moment lists.
        RecyclerView view = configureRecyclerView();
        view.setAdapter(adapter);
        adapter.beginObserving();

        // Expose the scan switch on the action bar.
        setSupportActionBar((Toolbar) findViewById(R.id.toolbar));

        // Expose the camera button.
        setFabClickHandler(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                takePhoto();
            }
        });
    }

    private void setFabClickHandler(View.OnClickListener listener) {
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(listener);
    }

    @Override
    public void onRestart() {
        super.onRestart();
        logState("onRestart");
    }

    @Override
    public void onPause() {
        super.onPause();
        mSensorManager.unregisterListener(this);
        logState("onPause");
    }

    @Override
    public void onStop() {
        super.onStop();
        logState("onStop");
    }

    /**
     * Before calling this, must have permission to read/write to storage, to
     * read/write photo data. Don't need camera permission, since a new activity
     * is started to actually take the photo.
     */
    private void takePhoto() {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        mCurrentPhotoId = Id.makeRandom();
        Integer ordinal = mLocalMoments.size() + 1;
        Uri uri = mBitMapper.getCameraPhotoUri(ordinal);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
        startActivityForResult(intent, RequestCode.CAPTURE_IMAGE);
    }

    /**
     * On new permissions, assume it's a fresh install and wipe the working
     * directory.   File formats and naming might have changed.  This is not
     * good for permanent photo management obviously.
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) {
        logState("onRequestPermissionsResult");
        if (mPermissionManager.granted(requestCode, permissions, results)) {
            FileUtil.initializeDirectory(Config.getWorkingDirectory(this));
            return;
        }
        toast(getString(R.string.need_permissions));
    }

    /**
     * A more realistic example would obtain the phone owner's name or email
     * address from, say, the contacts list.  Instead using a device
     * identifier.
     */
    public String getAuthorName() {
        return DEVICE_SIGNATURE;
    }

    /**
     * Could prompt the user for this; using a label derived from the id
     * instead.
     */
    public String getCaption(int index) {
        return "Generated caption " + index;
    }

    @Override
    protected void onStart() {
        super.onStart();
        logState("onStart");
        if (mLocalMoments.isEmpty()) {
            Log.d(TAG, "Loading moments from prefs.");
            mStateStore.prefsLoad(mLocalMoments);
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_NORMAL);
        logState("onResume");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        logState("onDestroy");
        if (mScanner.isScanning()) {
            mScanner.stop();
        }
        stopAllAdvertising();
        mV23Manager.shutdown();
        Log.d(TAG, "Destruction complete.");
        Log.d(TAG, " ");
        Log.d(TAG, " ");
    }

    private void stopAllAdvertising() {
        int count = 0;
        for (Advertiser advertiser : mAdvertiserFactory.allAdvertisers()) {
            if (advertiser.isAdvertising()) {
                try {
                    advertiser.stop();
                    count++;
                } catch (Exception e) {
                    e.printStackTrace();
                }
                Log.d(TAG, "Stopped advertising " + advertiser.toString());
            } else {
                Log.d(TAG, "A moment was not advertising");
            }
        }
        Log.d(TAG, "Stopped " + count + " advertisements.");
    }

    private RecyclerView configureRecyclerView() {
        RecyclerView view = (RecyclerView) findViewById(R.id.all_moments);
        view.setLayoutManager(makeLayoutManager());
        // Add some lines between items
        view.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST));
        // This makes it faster, but at a cost.
        view.setHasFixedSize(true);
        return view;
    }

    private LinearLayoutManager makeLayoutManager() {
        LinearLayoutManager mgr = new LinearLayoutManager(this);
        mgr.setOrientation(LinearLayoutManager.VERTICAL);
        mgr.scrollToPosition(0);
        return mgr;
    }

    private void toast(final String msg) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
            }
        });
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        logState("onCreateOptionsMenu");
        getMenuInflater().inflate(R.menu.menu_main, menu);
        MenuItem item = menu.findItem(R.id.action_scan);
        SwitchCompat sw = (SwitchCompat) MenuItemCompat.getActionView(item);
        mScanSwitchHolder.setSwitch(sw);
        if (mShouldBeScanning && !sw.isChecked()) {
            sw.setChecked(true);
        }
        return true;
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        logState("onActivityResult");
        if (requestCode == RequestCode.CAPTURE_IMAGE) {
            if (resultCode == RESULT_OK) {
                processCapturedPhoto();
            }
        }
    }

    private void processCapturedPhoto() {
        int ordinal = mLocalMoments.size() + 1;
        final Moment moment = mMomentFactory.make(mCurrentPhotoId, ordinal, getAuthorName(), getCaption(ordinal));
        Log.d(TAG, "pcp:     moment = " + moment);
        Log.d(TAG, "pcp:  list size = " + mLocalMoments.size());
        mLocalMoments.push(moment);
        mSerialExecutor.submit(new Runnable() {
            @Override
            public void run() {
                mBitMapper.dealWithCameraResult(mLocalMoments, moment);
            }
        });
    }

    @Override
    protected void onSaveInstanceState(Bundle b) {
        super.onSaveInstanceState(b);
        logState("onSaveInstanceState");
        b.putBoolean(B.SHOULD_BE_SCANNING, mScanner.isScanning());
        mStateStore.bundleSave(b, mRemoteMomentCache.values());
        mStateStore.prefsSave(mLocalMoments);
        b.putBoolean(BundleField.REMOTE_INSPECTION.toString(), mRemoteInspectionEnabled);
    }

    private void initializeOrRestore(Bundle b) {
        mStateStore.prefsLoad(mLocalMoments);
        if (b == null) {
            Log.d(TAG, "No bundle passed, starting fresh.");
            mRemoteMomentCache.clear();
            return;
        }
        Log.d(TAG, "Reloading from bundle.");
        mShouldBeScanning = b.getBoolean(B.SHOULD_BE_SCANNING, false);
        mStateStore.bundleLoad(b, mRemoteMomentCache);
        mRemoteInspectionEnabled = b.getBoolean(BundleField.REMOTE_INSPECTION.toString());
        if (mRemoteInspectionEnabled) {
            mV23Manager.enableRemoteInspection();
        }
    }

    private enum BundleField {
        REMOTE_INSPECTION
    };

    private String nameOfSharedPrefs() {
        return getClass().getPackage() + "." + getString(R.string.photo_file_prefix);
    }

    private void logState(String state) {
        Log.d(TAG, state + " --------------------------------------------");
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        float gX = event.values[0] / SensorManager.GRAVITY_EARTH;
        float gY = event.values[1] / SensorManager.GRAVITY_EARTH;
        float gZ = event.values[2] / SensorManager.GRAVITY_EARTH;
        double gForce = Math.sqrt(gX * gX + gY * gY + gZ * gZ);
        if (gForce < SHAKE_THRESHOLD) {
            return;
        }
        final long now = System.currentTimeMillis();
        if (now - mShakeTimestamp < SHAKE_EVENT_MS) {
            return;
        }
        mShakeTimestamp = now;

        final EditText invitee = new EditText(this);
        invitee.setInputType(InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);

        AlertDialog.Builder builder = new AlertDialog.Builder(this)
                .setTitle(getString(R.string.invite_remote_inspector)).setView(invitee)
                .setPositiveButton(getString(R.string.invite_remote_inspector_positive_button),
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                Intent intent = new Intent(Intent.ACTION_SEND);
                                String to = invitee.getText().toString();
                                intent.setType("message/rfc822");
                                intent.putExtra(Intent.EXTRA_EMAIL, new String[] { to });
                                intent.putExtra(Intent.EXTRA_SUBJECT, "Please help me debug");
                                try {
                                    intent.putExtra(Intent.EXTRA_TEXT,
                                            mV23Manager.inviteInspector(to, Duration.standardDays(1)));
                                    mRemoteInspectionEnabled = true;
                                } catch (Exception e) {
                                    toast(e.toString());
                                    return;
                                }
                                if (intent.resolveActivity(getPackageManager()) != null) {
                                    startActivity(intent);
                                } else {
                                    toast(getString(R.string.invite_remote_inspector_failed));
                                }
                            }
                        })
                .setNegativeButton(getString(R.string.invite_remote_inspector_negative_button),
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                dialog.cancel();
                            }
                        });
        if (mRemoteInspectionEnabled) {
            builder.setNeutralButton(getString(R.string.invite_remote_inspector_neutral_button),
                    new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            // This doesn't take effect till the next time the activity is created
                            // (invited users will still be able to connect till then).
                            mRemoteInspectionEnabled = false;
                        }
                    });
        }
        builder.show();
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
    }

    private static class RequestCode {
        static final int PERMISSIONS = 1001;
        static final int CAPTURE_IMAGE = 1003;
    }

    private static class B {
        static final String SHOULD_BE_SCANNING = "SHOULD_BE_SCANNING";
    }

}