Local Trader Application for LocalBitcoins

featured_localtrader

Local Trader is an application for LocalBitcoins. Use Local Trader to create and update advertisements, manage trades, send and download message attachments, release and fund trades, contact advertisers, or to handle payments. With LocalBitcoins you can trade Bitcoin and Ethereum locally or online.

SECURITY
Local Trader uses secure Oauth2 authentication for the LocalBitcoins services.

For authentication you will be taken to the LocalBitcoins website where you will need to authenticate as you would when logging into LocalBitcoins. Upon successful authentication you will be returned to the Local Trader application.

Local Trader request permissions for read, write and money_pin during authentication. The money_pin permission allows you to send money using your LocalBitcoins PIN code.

You created a PIN code as part of the LocalBitcoins registration and it is required to send Bitcoin from your wallet, and to fund or release trades. The PIN code is not the same as your 2-Factor Authentication (2FA).

View or manage your PIN code from your Profile page or use the mirror site for blocked regions.

PERMISSIONS EXPLAINED
Location – used for searching advertisers and creating new advertisement, but only if user requests it and gives the application permission.
Write sync settings – used for background syncing data for notifications.
Authenticate accounts – used for application authority for syncing data.

CONTACT:
LocalBitcoins Registration
LocalBitcoins Support
Reddit Community

[please note that comments are closed, please use the Reddit Community for feedback]

Setting Android Things Time and Time Zone

Worth noting for those looking to set the time and time zone on their Android Things boards.  I found the method that works in a Github repository (a Bluetooth sample project).  Also here is a list of time zones: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.

Setting the Time

You can set the system clock date/time manually on your Android Things board with the date shell command over ADB. By default, the command accepts a new date in the MMddHHmmYYYY.ss format:

# Reboot ADB into root mode
$ adb root

# Set the date to 2017/12/13 12:00:00
$ adb shell date 123112002017.00

Setting the Time Zone

You can set the system time zone manually by updating the persist.sys.timezone system property over ADB.

# Reboot ADB into root mode
$ adb root

# Set the time zone to US Mountain Time
$ adb shell setprop persist.sys.timezone "America/Denver"

Android Service startForeground and stopForeground tip

I had a use case for an application that required a long running Service (I know they are always frowned upon). The service seemed to be getting killed during the night and stuck in the “restarting” stage (Settings > Apps > Running). The Service would never restart at some point after being killed. So to raise its priority I used startForeground().

The Service already uses notifications so it wasn’t a big deal to have one required by startForeground(). The problem I ran into was when using stopForeground() to remove the the Service from the foreground followed by a stopService() call. This seemed to kill my Notification which I wanted to remain displaying the results of the service.

By default stopForeground() removes all Notifications when the service is stopped while still in the foreground:

“if you stop the service while it’s still running in the foreground, then the notification is also removed.”

What I concluded was that the order of the stopForeground() service is important. You have to create a notification after you use stopForeground() and before you use stopSelf() in order to preserve your last Notification. Just using stopForeground(false) does not keep the notification around if the service is then stopped (stopSelf()).

This might be must a niche use case (or corner case) but it’s one I needed and thought I might share.

-Mr

Stand-Up (Standing) Desk Designs

Updated 5/24/2013

Finally after more than a month the finished standing desk is complete. Here is a photo of the desk when I reviewed the final product in the store. I found a furniture store that makes custom wooden furniture named Banaco (bacano.com.ar‎). I chose this store because the furniture they created was not only beautiful, but it it had a mid-century flare.

Standing Desk
In the store.

Standing Desk
At home

DSCF6611
The view

Standing Desk
In use

The final desk ended up being a combination of features from Desk A and D designs. The main difference is the footrest now has a metal guard to project the wood when in the seated position.

Standing Desk (Original)

Lately I have been obsessed with stand-up or some call them standing desks. I work a lot from home and sitting all day isn’t ideal. There is no question of the health benefits (“Are You Sitting Down? Why a Stand-Up Desk Might Save Your Life” ) and history of standing desks (“Become a Stand-Up Guy: The History, Benefits, and Use of Standing Desks”). Many famous people, including Hemingway, used a standing desk for their work.

One of the most interesting designs I have found has been from a company called Tinkering Monkey. However, since they are located in Oakland CA and only deliver locally, I was left with designing and building (having it built for me rather) my own desk.

Building your own standing desk gives you some liberty over the design and color. Rather than just straight industrial raw wood look, I wanted something that fits with furniture I already own. For my designes I borrowed heavily from Tinkering Monkey (footrest, measuring elbow height for table top) but added some features I would like in my own desk such as castors (or levelers) and a mid-century modern finish.

Here are my first set of designs:

StandUp Desk Design A

StandUp Desk Design B

StandUp Desk Design D

StandUp Desk Design E

Android: TextView with Ellipsis Listener

I created an extension of the Android TextView component to adds the ability to receive updates when the text has an ellipses added. Short and sweet, here is the code and the link for the gist:

/**
 * Author: Michael Ritchie, ThanksMister LLC
 * Date: 10/16/12
 * Web: thanksmister.com
 *
 * Extension of <code>TextView</code> that adds listener for ellipses changes.  This can be used to determine
 * if a TextView has an ellipses or not.
 *
 * Derived from discussion on StackOverflow:
 *
 * http://stackoverflow.com/questions/4005933/how-do-i-tell-if-my-textview-has-been-ellipsized
 */
package com.cg.mobile.components;

import android.content.Context;
import android.text.Layout;
import android.util.AttributeSet;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;

public class EllipsisTextView extends TextView
{
    public interface EllipsisListener
    {
        void ellipsisStateChanged(boolean ellipses);
    }

    private final List<EllipsisListener> ellipsesListeners = new ArrayList<EllipsisListener>();

    private boolean ellipses;

    public EllipsisTextView(Context context)
    {
        super(context);
    }

    public EllipsisTextView(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }

    public EllipsisTextView(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);
    }

    public void addEllipsesListener(EllipsisListener listener)
    {
        if (listener == null) {
            throw new NullPointerException();
        }
        ellipsesListeners.add(listener);
    }

    public void removeEllipsesListener(EllipsisListener listener)
    {
        ellipsesListeners.remove(listener);
    }

    public boolean hadEllipses() {
        return ellipses;
    }

    @Override
    public void layout(int l, int t, int r, int b)
    {
        super.layout(l, t, r, b);

        ellipses = false;
        Layout layout = getLayout();
        if ( layout != null){
            int lines = layout.getLineCount();
            if ( lines > 0)  {
                if ( layout.getEllipsisCount(lines-1) > 0) {
                    ellipses = true;
                }
            }
        }

        for (EllipsisListener listener : ellipsesListeners) {
            listener.ellipsisStateChanged(ellipses);
        }
    }
}

Gist: https://gist.github.com/3902660

-Mr

Loading local JSON issue with Sencha, PhoneGap and Android 4.0.3

This is just a little tip when trying to load local .json files using the Ext.data.Store ajax proxy type. The following code will work in Android 4.0.3+ but you need to be sure to add an extra couple lines to get it to work, noCache: false and enablePagingParams: false. Without those lines the code still works fine in Android 2.3.3 but for Android 4 and above, the ajax fails on loading local .json files because the requests url includes parameters added by Sencha.

Roads DataStore

Ext.define("Roads", {
    extend:"Ext.data.Store",
    requires:"Ext.data.proxy.LocalStorage",
    config:{
        fields: [
            { name: 'id', type: 'int' },
            { name: 'title', type: 'string' },
            { name: 'subtitle', type: 'string' }
        ],
        proxy:{
            type:'ajax',
            url:'resources/data/roads.json',
            reader:{
                type:'json',
                           noCache: false,
            enablePagingParams: false,
                rootProperty:'roads'
            }
        },
        listeners:{
            load:function (s, r) {
                console.log(r)
            }
        }
    }
});

roads.json

{"roads":[
    {
        "id":"i10",
        "title":"Interstate 10 Arizona",
        "subtitle":"Papago Freeway/Maricopa Freeway"
    },
    {
        "id":"i17",
        "title":"Interstate 17 Arizona",
        "subtitle":"Black Canyon Freeway"
    },
    {
        "id":"l101",
        "title":"Arizona State Route 101",
        "subtitle":"Loop 101"
    }
]}

References where the solution was found:

http://www.sencha.com/forum/showthread.php?190878-Android-local-json-store-not-loading

http://www.sencha.com/forum/showthread.php?162322-Sencha-Touch-2-PhoneGap-are-not-working-on-Android-4/page3

-Mister

Handle Android hardware back button using PhoneGap and Sencha Touch 2

Lately I have been experimenting with PhoneGap and Sencha Touch 2 recently and discovered an issue with accessing the hardware back button for Android applications. What you want is the back button to navigate back through the views of the application rather than exit, unless of course you are on the first screen of your application. This is the default user experience on Android phones when pressing the hardware back button, so it’s a good thing to implement it.

My example is a modification of the Notes application Sencha Touch 2 example (found at http://miamicoder.com/2012/how-to-create-a-sencha-touch-2-app-part-1/) for PhoneGap delivery. This is a great multi-part series that walks you through the Sencha Touch 2 MVC framework to create a simple Notes application.
I used a Sencha Routes to navigate the history and a PhoneGap method for overriding the back button behavior for Android.

The first step was getting Sencha to play nice with PhoneGap when developing for Android 4+. The trick is to import each of your JS files within the index.html.

<!DOCTYPE html>
 <html>
 <head>
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1">

     <title>PhoneGap Notes Application</title>
     <link rel="stylesheet" href="css/master.css" type="text/css">
     <link rel="stylesheet" href="lib/resources/css/sencha-touch.css" type="text/css">

     <script type="text/javascript" charset="utf-8" src="lib/cordova-1.8.1.js"></script>

     <script type="text/javascript" src="lib/builds/sencha-touch-all-compat.js"></script>

     <!-- this had to be done for Android 4+ to work -->
     <script type="text/javascript" src="app/model/Note.js"></script>
     <script type="text/javascript" src="app/store/Notes.js"></script>
     <script type="text/javascript" src="app/controller/Notes.js"></script>
     <script type="text/javascript" src="app/view/NotesListContainer.js"></script>
     <script type="text/javascript" src="app/view/NotesList.js"></script>
     <script type="text/javascript" src="app/view/NoteEditor.js"></script>

     <script type="text/javascript" src="app.js"></script>
 </head>
 <body></body>
 </html>

Now we set up the app.js file to add the hooks for capturing the Android back button:

app.js

Ext.application({
    name: "NotesApp",
    models: ["Note"],
    stores: ["Notes"],
    controllers: ["Notes"],
    views: ["NotesList", "NotesListContainer", "NoteEditor"],

    launch: function () {

        var notesListContainer = {
            xtype: "noteslistcontainer"
        };
        var noteEditor = {
            xtype: "noteeditor"
        };

        Ext.Viewport.add([notesListContainer, noteEditor]);

        // set up a listener to handle the back button for Android 
        if (Ext.os.is('Android')) {
          document.addEventListener("backbutton", Ext.bind(onBackKeyDown, this), false);  // add back button listener

          function onBackKeyDown(e) {
              e.preventDefault();

              // you are at the home screen
              if (Ext.Viewport.getActiveItem().xtype == notesListContainer.xtype ) {
                  navigator.app.exitApp();
              } else {
                  this.getApplication().getHistory().add(Ext.create('Ext.app.Action', {
                      url: 'noteslistcontainer'
                  }));
              }
          }
       }
    }
});

I used the Ext.bind method so I could have access to “this” within the scope of the “onBackKeyDown” function. Notice that if the application is not currently on the “notesListContainer” view port we add a new url to the application history. This is part of using Sencha Routes, we will later use this in the main controller to navigate the application in the Notes.js controller file:

app/controller/Notes.js

Ext.define("NotesApp.controller.Notes", {

    extend: "Ext.app.Controller",
    config: {
        refs: {
            // We're going to lookup our views by xtype.
            notesListContainer: "noteslistcontainer",
            noteEditor: "noteeditor"
        },
        control: {
            notesListContainer: {
                // The commands fired by the notes list container.
                newNoteCommand: "onNewNoteCommand",
                editNoteCommand: "onEditNoteCommand"
            },
            noteEditor: {
                // The commands fired by the note editor.
                saveNoteCommand: "onSaveNoteCommand",
                deleteNoteCommand: "onDeleteNoteCommand",
                backToHomeCommand: "onBackToHomeCommand"
            }
        },
        routes: {
            'noteslistcontainer': 'activateNotesListenerPage'
        }
    },

    // override back button navigation and navigate back if viewing note editor
    activateNotesListenerPage: function ()
    {
        this.activateNotesList();
    },

    // handle new note command
    onNewNoteCommand: function ()
    {
        console.log("onNewNoteCommand");

        var now = new Date();
        var noteId = (now.getTime()).toString() + (this.getRandomInt(0, 100)).toString();

        var newNote = Ext.create("NotesApp.model.Note", {
            id: noteId,
            dateCreated: now,
            title: "",
            narrative: ""
        });

        this.activateNoteEditor(newNote);
    },

    // handle edit note command
    onEditNoteCommand: function (list, record) {

        console.log("onEditNoteCommand");
        this.activateNoteEditor(record);

    },

    // handle save note command
    onSaveNoteCommand: function () {

        console.log("onSaveNoteCommand");

        var noteEditor = this.getNoteEditor();

        var currentNote = noteEditor.getRecord();
        var newValues = noteEditor.getValues();

        // Update the current note's fields with form values.
        currentNote.set("title", newValues.title);
        currentNote.set("narrative", newValues.narrative);

        var errors = currentNote.validate();

        if (!errors.isValid()) {
            Ext.Msg.alert('Wait!', errors.getByField("title")[0].getMessage(), Ext.emptyFn);
            currentNote.reject();
            return;
        }

        var notesStore = Ext.getStore("Notes");

        if (null == notesStore.findRecord('id', currentNote.data.id)) {
            notesStore.add(currentNote);
        }

        notesStore.sync();

        notesStore.sort([{ property: 'dateCreated', direction: 'DESC'}]);

        this.activateNotesList();
    },
    onDeleteNoteCommand: function() {
        console.debug("onDeleteNoteCommand");

        navigator.notification.confirm(
            'You want to delete this note?',  // message
            Ext.bind(onConfirm, this),              // callback to invoke with index of button pressed
            'Warning',            // title
            'No,Yes'          // buttonLabels
        );

        // process the confirmation dialog result
        function onConfirm(selectedButtonIndex) {
            if(selectedButtonIndex == '2') {
                var noteEditor = this.getNoteEditor();
                var currentNote = noteEditor.getRecord();
                var notesStore = Ext.getStore("Notes");
                notesStore.remove(currentNote);
                notesStore.sync();
                this.activateNotesList();
            }
        }
    },

    onBackToHomeCommand: function ()
    {
        console.log("onBackToHomeCommand");
        this.activateNotesList();
    },

    // EditNote Class functions
    getRandomInt: function (min, max)
    {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    },

    activateNoteEditor: function (record)
    {
        var noteEditor = this.getNoteEditor();
        noteEditor.setRecord(record); // load() is deprecated.

        this.getApplication().getHistory().add(Ext.create('Ext.app.Action', {
            url: 'noteslistcontainer/noteeditor'
        }));

        Ext.Viewport.animateActiveItem(noteEditor, this.slideLeftTransition);
    },

    activateNotesList: function ()
    {
        Ext.Viewport.animateActiveItem(this.getNotesListContainer(), this.slideRightTransition);
    },

    // transitions
    slideRightTransition: { type: 'slide', direction: 'right' },
    slideLeftTransition: { type: 'slide', direction: 'left' },

    // Base Class functions.
    launch: function () {
        this.callParent(arguments);
        Ext.getStore("Notes").load();
        console.log("launch");
    },
    init: function () {
        this.callParent(arguments);
        console.log("init");
    }
});

Here we setup a route within the config that will call the method “activateNotesList” when the url “noteslistcontainer” is added to the history. If the user creates/edits a new note, the method in the controller that navigates the application to the NoteEditor view adds to the history a new url for “noteslistcontainer/noteeditor”.

Hitting the Android hardware back button from the NoteEditor view is captured by our “onBackKeyDown” handler in the app.js file. Since the active container is not the NotesListContainer view, the history url will be set to “noteslistcontainer” and the method “activateNotesList” will be fired. If our active container was the NotesListContainer, the application would exit as normal.

That is pretty much it. The rest of the code isn’t modified from the Notes example (I didn’t do step 5 of that series). I am sure there is probably more than a few ways to implement the proper behavior for the Android hardware back button, but for me this works. If you have any other suggestions or improvements, do pass them along.

Source References:

Sencha Touch Routes and Back Button
How to create a Sencha Touch 2 app

-Mister

Android: Removing Fade Effect on ActionBar when using setActionView()

Letting me start by explaining the desired effect I was trying to achieve. On the Android Google+ application if you hit refresh there is a spinner animation that replaces the refresh ActionBar item when pressed. I was able to achieve this but setting the pressed MenuItem to a new layout by using “MenuItem.setActionView()”.

However, on ICS if you have a two ore more MenuItems always available in the Android ActionBar (i.e. you set android:showAsAction=”ifRoom” in the mainmenu.xml file below) swapping the the icon on one MenuItem when pressed causes a slight ghost selection of the other.

The MenuItem next to the one pressed immediately shows a highlight as you stop pressing the refresh icon. This ghost selection behavior is produced because the default ICS background selector contains a fade animation. Swapping the icon causes the animation up/fade to transfer to the next non-customized ActionBar MenuItem, the one you didn’t select.

Here is a code example (not complete) on how to swap out the MenuItem with a custom spinner:

private View mRefreshIndeterminateProgressView; // save inflated layout for reference
private MenuItem refreshItem; // reference to actionbar menu item we want to swap

if (mRefreshIndeterminateProgressView == null) {
   LayoutInflater inflater = (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
   mRefreshIndeterminateProgressView = inflater.inflate(R.layout.actionbar_indeterminate_progress, null);
}
refreshItem.setActionView(mRefreshIndeterminateProgressView); // replace actionbar menu item with progress
// doing refreshItem.setActionView(null) removes the animation

Here is what the actionbar_indeterminate_progress.xml looks like:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" 
    android:gravity="center"> 
<ProgressBar android:layout_width="16dp" 
    android:layout_height="16dp" 
    android:layout_marginLeft="12dp" 
    android:layout_marginRight="12dp" 
    android:layout_gravity="center" 
    style="?android:attr/indeterminateProgressStyle"/> 
</FrameLayout> 

Here is your mainmenu.xml:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

<item android:id="@+id/detail_menu_refresh" 
    android:title="Refresh" 
    android:icon="@drawable/ic_action_refresh" 
    android:showAsAction="ifRoom"/> 

 <item android:id="@+id/main_menu_share" 
    android:title="Share" 
    android:icon="@drawable/ic_action_share"
    android:showAsAction="ifRoom"/>

 <item android:id="@+id/main_menu_about" 
    android:title="About" 
    android:icon="@drawable/ic_action_about"
    android:showAsAction="never"/>

 </menu>

The solution is to set your own custom selector background for the ActionBar so you can remove the fade. To do this, I grabbed the abs__item_background_holo_dark.xml from (ActionBarSherlock > Library > res > drawable) and the associated graphics to add to my project. You could easily do this without using ActionBarSherlock and just ActionBar and regular themes for your project. I then removed the following line from the nod in the xml which disables the fade:

 android:exitFadeDuration="@android:integer/config_mediumAnimTime" 

Here is modified abs__item_background_holo_dark.xml:

 
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Even though these two point to the same resource, have two states so the drawable will invalidate itself when coming out of pressed state. -->
    <item android:state_focused="true"  android:state_enabled="false" android:state_pressed="true" android:drawable="@drawable/abs__list_selector_disabled_holo_dark" />
    <item android:state_focused="true"  android:state_enabled="false" android:drawable="@drawable/abs__list_selector_disabled_holo_dark" />
    <item android:state_focused="true"  android:state_pressed="true" android:drawable="@drawable/abs__list_selector_background_transition_holo_dark" />
    <item android:state_focused="false" android:state_pressed="true" android:drawable="@drawable/abs__list_selector_background_transition_holo_dark" />
    <item android:state_focused="true"  android:drawable="@drawable/abs__list_focused_holo" />
    <item  android:drawable="@android:color/transparent" />
</selector>

Here is how I set it up in my styles.xml file:

 
<style name="Dark" parent="Theme.Sherlock.Light.DarkActionBar">
    <item name="actionBarStyle">@style/Widget.Styled.ActionBar</item>
    <item name="android:actionBarStyle">@style/Widget.Styled.ActionBar</item>
    <item name="actionBarItemBackground">@drawable/abs__item_background_holo_dark</item>
    <item name="android:actionBarItemBackground">@drawable/abs__item_background_holo_dark</item>
</style>

<style name="Widget.Styled.ActionBar" parent="Widget.Sherlock.Light.ActionBar.Solid.Inverse"/>

I got a great tip on how to fix this from Jake Wharton, the creator of ActionBarSherlock. I recommend using his library if you want to have a consistent ActionBar across all versions of your Android applications.

– Mister

Android: Changing the Default Indeterminate Progress Size in ActionBarSherlock

Just a quick post on how to change the default size of the Indeterminate Progress animation when using ActionBarSherlock (ABS). This uses the dark halo them for Android 4.0.1 but tested and working from version 2.3.2 to 4.0.1 of the SDK. Here are the before and after shots:

Grab the progress_small_holo.xml and associated images from the Android SDK (15) and move them to your project (from your SDK location: android/platforms/android-15/data/res/drawable). We will be using this to style the progress animation in the ActionBar for ABS Set up your style.xml as follows:

values/styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Dark" parent="Theme.Sherlock">
        <item name="actionBarStyle">@style/Widget.Styled.ActionBar</item>
        <item name="android:actionBarStyle">@style/Widget.Styled.ActionBar</item>
    </style>

    <style name="Widget.Styled.ActionBar" parent="Widget.Sherlock.Light.ActionBar">
        <item name="android:indeterminateProgressStyle">@style/IndeterminateProgress</item> 
        <item name="indeterminateProgressStyle">@style/IndeterminateProgress</item> 
    </style>

    <style name="IndeterminateProgress" parent="@android:style/Widget.ProgressBar.Small"> 
       <item name="android:indeterminateDrawable">@drawable/progress_small_holo</item> 
   </style> 
</resource>

values-v14/styles.xml (ICS)

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Dark" parent="Theme.Sherlock">
        <item name="actionBarStyle">@style/Widget.Styled.ActionBar</item>
        <item name="android:actionBarStyle">@style/Widget.Styled.ActionBar</item>
    </style>

    <style name="Widget.Styled.ActionBar" parent="Widget.Sherlock.Light.ActionBar">
        <item name="android:indeterminateProgressStyle">@style/IndeterminateProgress</item> 
        <item name="indeterminateProgressStyle">@style/IndeterminateProgress</item> 
    </style>

    <style name="IndeterminateProgress" parent="@android:style/Widget.ProgressBar.Small"/> 

</resource>

You could also add some additional sizing if you want to center the progress animation in the actionbar:

<style name="IndeterminateProgress" parent="@android:style/Widget.ProgressBar.Small">
       <item name="android:minWidth">48dp</item> 
</style>

You can see more of the discussion on this thread or from StackOver.

-Mister

Android: Null data returned from Camera Intent

Anyone who has tried calling the image or video capture intent using the default Camera activity probably has been met with much frustration. There are many approaches and workarounds for various phones and API levels because of the insane fragmentation of Android. Most of us just want a simple way to call the default Camera activity, have the video or image stored in the Gallery, and retrieve a the Camera intent results for further processing.

The Android documentation provides what appear to be a very straight forward way to capture images and video and either save them in the default location or a folder of your choosing. The resources available for doing so can be found here:

Image capture intent
Saving Media Files

However, I am not sure Google actually tests their Android examples on real phones (developer phones). Testing the method for capturing images on a Galaxy Nexus or Nexus One both return a null value for the data when receiving the camera intent result. However, capturing video seems to work as expected. Though both the image and video files are written to the specified folders on the device.

So naturally, as a developer you are stuck with a borked example that must be modified for a real world implementation. Luckily for my needs I didn’t need to store images or video in an external folder, I wanted the camera application to store the requested media in the default location with a default name. This is usually the Camera folder.

Below is my modification to the onActivityResult() method of the Activity for retrieving the Uri of the captured image. I have left the rest of the code from the Android Camera example for storing video and images in the activity in case you are feeling particularly brave and want to store your images in a different location.

You might ask why I couldn’t insert the video into the MediaStore the same way I am inserting the captured image (by passing the Uri and ContentValues). Doing this for video actually created two files for me, one in the Media Store, and an 0kb file in the external video folder on the SD card. This only happened on the Nexus One (Android 2.3.4) and not the Galaxy Nexus (4.0.2). So you end up with two different methods for making sure media appears in the MediaStore.

AndroidCameraTestsActivity

package com.thanksmister.mobile;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;

import android.app.Activity;
import android.content.ContentValues;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;

public class AndroidCameraTestsActivity extends Activity 
{
	private static final String TAG = AndroidCameraTestsActivity.class.getSimpleName(); 
	
	private static final int CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE = 100;
	private static final int CAPTURE_VIDEO_ACTIVITY_REQUEST_CODE = 200;
	public static final int MEDIA_TYPE_IMAGE = 1;
    public static final int MEDIA_TYPE_VIDEO = 2;

    private Uri fileUri;
    
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) 
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

    /** 
     * https://developer.android.com/guide/topics/media/camera.html 
     * **/
    public void onCaptureImage(View v) 
    {
        // give the image a name so we can store it in the phone's default location
    	String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
    	
        ContentValues values = new ContentValues();
		values.put(MediaStore.Images.Media.TITLE, "IMG_" + timeStamp + ".jpg");

        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        
        //fileUri = getOutputMediaFileUri(MEDIA_TYPE_IMAGE); // create a file to save the image (this doesn't work at all for images)
        fileUri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); // store content values
		intent.putExtra( MediaStore.EXTRA_OUTPUT,  fileUri);
       
        // start the image capture Intent
        startActivityForResult(intent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE);
    }
    
    /** 
     * https://developer.android.com/guide/topics/media/camera.html 
     * **/
    public void onCaptureVideo(View v) 
    {
    	 //create new Intent
        Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);

        //fileUri = getOutputMediaFileUri(MEDIA_TYPE_VIDEO);  // create a file to save the video in specific folder (this works for video only)
        //intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);  // set the image file name
      
        intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1); // set the video image quality to high

        // start the Video Capture Intent
        startActivityForResult(intent, CAPTURE_VIDEO_ACTIVITY_REQUEST_CODE);
    }
    
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) 
    {
    	super.onActivityResult(requestCode, resultCode, data);
    	
        if (requestCode == CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE) {
            if (resultCode == RESULT_OK) {
            	
            	// Originally I was going to iterate through the list of images and grab last added to the MediaStore.
            	// But this is not necessary if we store the Uri in the image
            	/*
            	String[] projection = {MediaStore.Images.ImageColumns._ID};
            	String sort = MediaStore.Images.ImageColumns._ID + " DESC";

            	Cursor cursor = this.managedQuery(MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, projection, null, null, sort);

            	try{
            		cursor.moveToFirst();
            		Long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns._ID));
            		fileUri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id));
            	} finally{
            		cursor.close();
            	}
            	*/
                
				if(fileUri != null) {
					Log.d(TAG, "Image saved to:\n" + fileUri);
					Log.d(TAG, "Image path:\n" + fileUri.getPath());
					Log.d(TAG, "Image name:\n" + getName(fileUri)); // use uri.getLastPathSegment() if store in folder
				}
                
            } else if (resultCode == RESULT_CANCELED) {
                // User cancelled the image capture
            } else {
                // Image capture failed, advise user
            }
        }

        if (requestCode == CAPTURE_VIDEO_ACTIVITY_REQUEST_CODE) {
            if (resultCode == RESULT_OK) {
            	
                // Video captured and saved to fileUri specified in the Intent
            	fileUri = (Uri) data.getData();
				
				if(fileUri != null) {
					Log.d(TAG, "Video saved to:\n" + fileUri);
					Log.d(TAG, "Video path:\n" + fileUri.getPath());
					Log.d(TAG, "Video name:\n" + getName(fileUri)); // use uri.getLastPathSegment() if store in folder
				}
				
            } else if (resultCode == RESULT_CANCELED) {
                // User cancelled the video capture
            } else {
                // Video capture failed, advise user
            }
        }
    }
    
    /** Create a file Uri for saving an image or video to specific folder
     * https://developer.android.com/guide/topics/media/camera.html#saving-media
     * */
    private static Uri getOutputMediaFileUri(int type)
    {
          return Uri.fromFile(getOutputMediaFile(type));
    }

    /** Create a File for saving an image or video */
    private static File getOutputMediaFile(int type)
    {
        // To be safe, you should check that the SDCard is mounted
        
    	if(Environment.getExternalStorageState() != null) {
    		// this works for Android 2.2 and above
    		File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "AndroidCameraTestsFolder");
            
            // This location works best if you want the created images to be shared
            // between applications and persist after your app has been uninstalled.

            // Create the storage directory if it does not exist
            if (! mediaStorageDir.exists()) {
                if (! mediaStorageDir.mkdirs()) {
                    Log.d(TAG, "failed to create directory");
                    return null;
                }
            }

            // Create a media file name
            String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
            File mediaFile;
            if (type == MEDIA_TYPE_IMAGE){
                mediaFile = new File(mediaStorageDir.getPath() + File.separator +
                "IMG_"+ timeStamp + ".jpg");
            } else if(type == MEDIA_TYPE_VIDEO) {
                mediaFile = new File(mediaStorageDir.getPath() + File.separator +
                "VID_"+ timeStamp + ".mp4");
            } else {
                return null;
            }

            return mediaFile;
    	}
        
    	return null;
    }

    // grab the name of the media from the Uri
    protected String getName(Uri uri) 
	{
		String filename = null;

		try {
			String[] projection = { MediaStore.Images.Media.DISPLAY_NAME };
			Cursor cursor = managedQuery(uri, projection, null, null, null);

			if(cursor != null && cursor.moveToFirst()){
				int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME);
				filename = cursor.getString(column_index);
			} else {
				filename = null;
			}
		} catch (Exception e) {
			Log.e(TAG, "Error getting file name: " + e.getMessage());
		}

		return filename;
	}
}

Main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <Button android:text="Capture Image" android:onClick="onCaptureImage" 
        android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
    
	<Button android:text="Capture Video" android:onClick="onCaptureVideo"
	    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
	
</LinearLayout>

ApplicationManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.thanksmister.mobile"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:targetSdkVersion="15" android:minSdkVersion="8"/>
    
    <uses-permission android:name="android.permission.CAMERA"/>
 	<uses-feature android:name="android.hardware.camera" />
 	<uses-feature android:name="android.hardware.camera.autofocus"/>
  	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name="AndroidCameraTestsActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Additional Resources:

Android Camera Test Project
Null Intent passed back On Samsung Galaxy Tab…

-Mister