Showing posts with label programming. Show all posts
Showing posts with label programming. Show all posts

Friday, May 8, 2015

Google Maps kml files are now zipped kmz files

Apparently I'm bad at keeping up to date when APIs are changed. I looked at TacoTacoTruck recently and it failed to load any map data. It seems that instead of giving a plaintext kml file as it used to, Google Maps returns the file in zip format (kmz), even if you ask for a kml file.

So it needs to be unzipped. Here's some code:

class Fetch(webapp2.RequestHandler):
    def get(self):
        # urlToUse is just a little hardcoded map of a filename as key and the url as the value
        url = urlToUse[self.request.path]
        if url is None:
            print("unexpected path: %s" % self.request.path)
            return
        cachedData = memcache.get(self.request.path)
        if cachedData is not None:
            self.response.write(cachedData)
            return
        startDownloadTime = time.time()
        result = urlfetch.fetch(url)
        if result.status_code == 200:
            elapsed = time.time() - startDownloadTime
            print("downloaded %s in %s seconds" % (url, elapsed))
            bytes = io.BytesIO(result.content)
            zip = zipfile.ZipFile(bytes)
            # retrieve the file name of the first (and only) file in the zipfile
            firstName = zip.namelist()[0]
            print("unzipping %s" % firstName)
            # unzip it into contents
            contents = zip.read(firstName)
            memcache.set(key=self.request.path, value=contents, time=cacheTime)
            self.response.write(contents)
        else:
            print("can't fetch kml file for %s. Status=%s" % (self.request.path, result.status_code))
            self.response.write('')

Friday, May 1, 2015

Donors, thank you!

In the past two months I've received several in-app purchase donations for my Napkin paint program. Thank you, kind people!

Small but useful updates to MapTag

Due to the increasing number of user-generated panorama shots being uploaded to Google, it's been harder to find a game where you can actually navigate. I was playing today and got 4 user panoramas in a row! So I added a check to look for "Google" in the copyright info. This should nearly eliminate getting a user-generated pano.

Another minor annoyance came after you got the score window. You could click it and it would close, but the map where you made your guess didn't close along with it. Now it does.

Finally, I've added a new themed game called "New to the City." It places you in a random location but within a specific city. There's currently 24 cities to choose from, so go get lost!

Happy wandering!

Tuesday, July 8, 2014

Taco trucks

About a month ago I was browsing Reddit's SF Bay Area sub, and noticed a link to maps of taco trucks in the East Bay. Taco trucks take me back to my early days in Silly and my first job (in Mountain View), and a cow-orker who introduced me to the joys of a lengua taco dripping onto a paper plate. The redditor who made the map seemed interested in a web version, so I built TacoTacoTruck. However, he has yet to follow up on whether he liked it, hated it, or just didn't have time to deal. So although the map itself is not my own work, the web version is mine and could be fed any user-made Google map.

Thursday, July 3, 2014

Finally published Napkin Ideas on Play.

Well, finally.

This was actually done months ago, but I thought it would be really nice to have an ability to edit the background. So I started creating a wizard to do that, and for whatever reason, I just could not get it working the way I wanted, and got kind of depressed about that. So I dropped that bit out of the code and decided to publish as it was before.

Well, almost as it was before, with one notable exception: I took out the advertising. It's annoying to look at, and probably not going to generate any significant amount of revenue. In its place, I have put in what I'm calling a beg bar: give me a donation, the little yellow bar goes away.

Tuesday, January 14, 2014

Done with the paint app

There was a trip to Germany for the boyfriend to attend a conference, and Christmas, and New Year's, so not a whole lot got done between the 15th and the 2nd. But since then it's been chin-rubbing and poking and growling, and finally the app was finished. I still need code review but right now it does just about everything I want it to.

The sticky bit was adding text. I'd originally set up the button to open an alert dialog where you could enter your text, and select a font size via a slider. That was visually jarring and functionally awkward because you couldn't really tell how large to make your text in relation to the drawing.  You would only know after placing it, so you'd have to undo and try again if you found it the wrong size.

So I found a bit of code that would include an actionable graphic (an image of an X to clear the text) in an EditText. From that starting point I made the EditText so it would be placed directly on the drawing surface, and it could be resized by dragging at the corner graphic. Clicking the checkmark graphic at the top cements the text in place. It's much smoother than having a dialog box.

After that, some code to save the drawing in JSON so it can be retrieved later.

For the next trick I want to put advertising in it. I'd like have a free-with-ads version, and a paid no-ads version. Don't hurt me, please. I'm not crazy about doing it either.

So to distract you from that last paragraph, here's the code for an edittext that can be moved around and the font resized by dragging.  Both the drawing and the EditText are children of a Viewgroup.

public class MovableEditText extends EditText {

    // 2 drawables: an OK and a resize handle
    private Drawable closeImg = getResources().getDrawable(R.drawable.text_ok);
    private Drawable resizeImg = getResources().getDrawable(R.drawable.text_resize);
    private long timeFingerDown;
    public InputMethodManager imm;
    private App app;
    private boolean resizing;
    private TextCompletedListener textListener;
    private boolean finished = false;
    private float startDeltaLeft;
    private float startDeltaTop;

    float leftMargin = 100;
    float topMargin = 100;

    public MovableEditText(Context context) {
        super(context);
        app = (App) context.getApplicationContext();
        imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
        init();
    }

    @Override
    public void scrollTo(int x, int y) {
        super.scrollTo(0, 0);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                timeFingerDown = event.getEventTime();
                // Remembers where you grabbed the text
                startDeltaLeft = event.getX();
                startDeltaTop = event.getY();
                setCursorVisible(true);
                return true;

            case MotionEvent.ACTION_UP:
                resizing = false;
                // clicking on the OK checkbox
                if (event.getY() < getPaddingTop() + closeImg.getIntrinsicHeight()) {
                    MovableEditText.this.okButtonClick();
                }
                // otherwise, act like a proper edittext
                if (event.getEventTime() - timeFingerDown < 200 && !finished) {
                    super.onTouchEvent(event);
                }
                return false;

            case MotionEvent.ACTION_MOVE:
                if (event.getX() > getWidth() - getPaddingRight() - resizeImg.getIntrinsicWidth()) {
                    resizeBox(event);
                    // we touched the arrow, so we are resizing the box (i.e. increasing font size)
                    resizing = true;
                    return true;
                }
                if (event.getEventTime() - timeFingerDown > 200 && !resizing) {
                    // we are moving the box; close the keyboard
                    imm.hideSoftInputFromWindow(getWindowToken(), 0);
                    moveBox(event);
                }
                return true;

            case MotionEvent.ACTION_CANCEL:
                resizing = false;
                return false;
        }
        return true;
    }

    public void setTextCompletedListener(TextCompletedListener l) {
        textListener = l;
    }

    private void init() {
        setMinimumHeight(closeImg.getIntrinsicHeight()*2);
        setMinimumWidth(closeImg.getIntrinsicWidth()+resizeImg.getIntrinsicWidth() *2);
        // Set bounds of the Clear button so it will look ok
        closeImg.setBounds(0, 0, closeImg.getIntrinsicWidth(), closeImg.getIntrinsicHeight());
        resizeImg.setBounds(0, 0, resizeImg.getIntrinsicWidth(), resizeImg.getIntrinsicHeight());
        // There may be initial text in the field, so we may need to display the  button
        showOkButton();
        //if text changes, take care of the button
        this.addTextChangedListener(new TextWatcher() {
            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                MovableEditText.this.showOkButton();
            }

            @Override
            public void afterTextChanged(Editable arg0) {
            }

            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }
        });
    }

    private void moveBox(MotionEvent event) {
        leftMargin += (int) (event.getX() - startDeltaLeft);
        topMargin += (int) (event.getY() - startDeltaTop);
        requestLayout();
    }

    // the following is to be used by drawview when resizing and panning
    public void externalMove(float dragX, float dragY) {
        leftMargin = (int) (leftMargin + dragX);
        topMargin = (int) (topMargin + dragY);
        requestLayout();
    }

    private void resizeBox(MotionEvent event) {
        setWidth((int) event.getX());
        setHeight((int) event.getY());
        resizeText();
    }

    // change the text size to whatever will fit in the box
    private void resizeText() {
        String[] multiline = this.getText().toString().split("\n");
        float adjustedHeight = (getMeasuredHeight()/multiline.length - getCompoundPaddingBottom() - getCompoundPaddingTop())/2;
        setTextSize(adjustedHeight);
    }

    // if there is text, add the OK button
    void showOkButton() {
        if (this.getText().toString().equals("")) {
            // remove the clear button
            this.setCompoundDrawables(this.getCompoundDrawables()[0],null, resizeImg, this.getCompoundDrawables()[3] );
        } else {
            //add clear button
            this.setCompoundDrawables(this.getCompoundDrawables()[0], closeImg, resizeImg, this.getCompoundDrawables()[3]);
        }
    }

    void okButtonClick() {
        MyViewGroup viewGroup = (MyViewGroup) getParent();
        // get the text, get the size, get the position, create a Stroke from it
        // locate this view in the viewgroup's coordinate system
        // the screen coordinates minus the location of the MET
        Float deltaL = getLeft() - getScrollX() - viewGroup.dv.getCenterX();
        Float deltaB = getTop() - getScrollY() + getBaseline() - viewGroup.dv.getCenterY() ;
        // viewcenterxy are the canvas coordinates
        Float dvViewCenterX = viewGroup.dv.getViewCenterX();
        Float dvViewCenterY = viewGroup.dv.getViewCenterY();
        Float scaleFactor = viewGroup.dv.getScaleFactor();
        // translate that delta to the canvas's coordinates
        Float translatedL = dvViewCenterX + (deltaL/scaleFactor);
        Float translatedB = dvViewCenterY + (deltaB/scaleFactor);

        if (this.getText().toString().length()>0) {
            app.setText(getText().toString());
            app.setTextSize(getTextSize()/2);  // i do not know why it keeps doubling the size
            app.strokeInProgress = new TextStrokeBuilder(app, translatedL, translatedB, scaleFactor);
            app.finishStroke();
        }
        // tell viewgroup to delete the EditText
        textListener.onTextCompleted();
        finished = true;
        imm.hideSoftInputFromWindow(getWindowToken(),0);
    }

    public void setLeftMargin(float left) {
        leftMargin = left;
    }
    public void setTopMargin (float top) {
        topMargin = top;
    }
}

Thursday, December 12, 2013

getting close to done

I finally managed to build a resizable EditText box. So you click on the Add Text button, and a little grey box pops up onscreen on top of your drawing. It's semi-transparent so you can see what's going on below. The keyboard pops up and you type your stuff. A double-headed arrow icon on the right: left/right will increase the width of the box, up/down will change the size of the font. Touch the checkmark icon at the top and the text will be placed. Press & hold on the text itself will let you drag it around to where you want it.

This is getting nice. It's reasonably smooth and intuitive.

There are several things I need to fix to get it fully working right. At the moment it doesn't place the text where I want, and the size is being measured in actual pixels, so if you're zoomed out on the page, it's freaking teeny tiny. These are scaling issues. The other bit is gracefully deleting the EditText when finished -- I need a hook or a listener or something that the EditText can use to tell the app it's done.

Sunday, November 17, 2013

working on a new app

A few days before Halloween Brian settled on his "costume": a bow tie.

Not just any ol' bowtie, but

><  > the Bowtie of the Future <  ><

It would be changeable at a touch. It would set the background to the shirt behind it. It would take photos. It would have a +1 button.

In about 6 hours we cobbled together a bowtie image that would cycle through the Google colors and wiggle when clicked, had a fake +1 button and a counter on the left, a subtle (Google design always runs subtle) "BETA" on the right. "Adjusting" the tie, i.e. rotating the phone back and forth, would take a photo.

There were a bunch of little quirky things to code for. Because of the front camera's placement, the entire app had to be displayed upside-down from the normal landscape mode, otherwise the camera would have been obscured by his collar. Photos taken by the camera resulted as upside-down, but flipping is easy. Since he's tallish, the camera tended to take photos of the top edge of the opposite wall, and I experimented with carving wedges from packing foam until we decided it was more trouble than it was worth. Brian was dissatisfied with the shake code I'd found and fine-tuned it to use the gyroscope and a specific sequence  of motions to listen for.

And of course we didn't actually use the back camera to divine the clothing behind it. Brian picked a shirt, I slapped it on the scanner, and turned it into the background image.

But that isn't the 'new app' of the post title.  I've started a paint program. Nothing that isn't the same as quite a few paint apps out there, and in fact it may be simpler than most. But it's partly for the experience and partly because I have an idea that will make it special.

The basics, of course, include:
- a palette of 10 colors, which can be customized
- fill color
- outline thickness
- brush
- point-to-point lines
- rectangles and ovals
- text (nothing fancy, just plain ol' text using the native Android font in the outline color)
- zooming, panning
- undo
- eraser (you won't believe how hard that is to figure out, partially due to sketchy Android documentation).

All colors include transparency levels.

You can take a photo or select one from your gallery to slide in as the background layer.

You can save the drawing+background to the gallery as a jpg, and share it to email, Drive, etc.

All customizable items (colors, line thickness) are saved in SharedPreferences, so you can always have that perfect 20% transparent purple you love.

I've just finished coding all the above. Neither Brian nor I are fully satisfied with the sequence of events used to add text.  It's not easy to make a not-too-disruptive popup with a space for your text and a slider for your font size when there's a keyboard that will slide in and take up half your screen space.

More to come...

Tuesday, July 16, 2013

Fiddling and tweaking

Since releasing MapTag I've made a number of minor adjustments to the UI in response to comments.

However, it doesn't work in IE8 and that will not change.

The more fun part has been adding a visual index of saved games and previously played games which players completed in less than 10 minutes and less than 125km from the target. Click the "more games" tab to view that. It would be nice to dynamically adjust the criteria, but App Engine doesn't allow an inequality query on more than one field. It's a bit of a struggle to understand why it cannot do something that's elementary to any SQL query, until one considers its intended scalability.

So instead, I had to add a boolean field for 'successful', and if I want to change the criteria I have to run a bit of python script* to run through all the records and recompute it.  One hundred twenty-five kilometers is a pretty broad definition of success, but I wanted it a bit broad for countries that don't have a Roman-based alphabet, which is hard for us Roman alphabetters to Google for. For someplace like Thailand, if you get the right country, you still have a chance of "success."

* the server side I originally wrote in Java, but since Python is faster for certain datastore operations, the boyfriend recommended we switch to that. Did I mention I didn't know any Python? or hardly anything about server side programming?

Tuesday, June 4, 2013

Privacy policy for MapTag

I thought I'd better do one of these for MapTag, too. Same template, different stuffs, more comedy.

I am one human being doing this in my spare time. I'm a geek, which means I'm allergic to salesdroids, which means I would rather spear my own eyeballs out with red-hot pokers than market remotely-related stuff to you. If that's doesn't mean anything to you, here be the legalese:

Privacy Policy for MapTag

  • This application sets a cookie which indicates if you have been to the website before or not.
  • We do not receive or collect any personal information about you, such as your name, street address, phone number or email address. Google Analytics reports your country language, country, state/province and the nearest city. It also reports your browser and operating system (though not specific versions) and the name of your ISP.  If you live in a town of 50 people we might be able to figure out who you are by asking your kid, or their best friend, who in town is browsing the web using Safari on a Mac running OSX.  Of course, that means actually travelling to your town to ask this question, and frankly, we have better stuff to do.
  • We do not knowingly collect information from children under 13. If you believe we have inadvertently collected such information, please contact us so we can promptly obtain parental consent or remove the information. 
  • We do not receive, collect or use your precise geographic location.
  • There are no methods of seeing what data we collect from you.
  • We may keep data indefinitely, though since we don't use it other than to say stuff like, "Oh, someone from Tanzania is playing, awesome!" we have no reason to keep it.
  • We do not get any personally identifiable information (such as name, street address, email or phone) much less share it with other companies.  We might consider making some up, though.  I hear advertisers pay for that stuff.
  • We do not knowingly allow advertising companies to collect data through our service for ad targeting. However, be aware that the social media buttons that are included in this game (Facebook, G+, Twitter)  means those social media networks can probably figure out that you played MapTag. If you don't want that, Chrome has a handy extension called Disconnect which disables 3rd-party tracking.
  • We take reasonable steps to secure your information against unauthorized access or disclosure. However, no security or encryption method can be guaranteed to protect information from hackers or human error.
  • Information we collect may be stored or processed on computers located in any country where we do business.
  • We may make anonymous information available to third parties in these limited circumstances: (1) with your express consent, (2) when we have a good faith belief it is required by law, (3) when we have a good faith belief it is necessary to protect our rights or property, or (4) to any successor or purchaser in a merger, acquisition, liquidation, dissolution or sale of assets. Your consent will not be required for disclosure in these cases, but we will attempt to notify you, to the extent permitted by law to do so.
  • This privacy policy was last updated on 6/4/2013. Our privacy policy may change from time to time. If we make any material changes to our policies, we will place a prominent notice on our website or application. 

If you have any questions or concerns about our privacy policies, please contact fallinghawks@gmail.com.

Friday, May 31, 2013

almost ready to submit to the games sites

Changes and additions:
  • I did add more locations. I forgot to do Israel, which is a darned interesting place to look at.  We have Bangkok, Chiang Mai, Singapore, and Taiwan.  I also added three US urban areas in an attempt to increase the chances of an urban area being selected: SF, LA, and NY (and surrounding areas of each).  Moscow was already included in the Europe ring. This seems to have reduced the chances of being on a long, straight, empty road.
  • I wrote another Javascript/JQuery/HTML file to give players a map, let them navigate in and out of StreetView, and select the location they're at as the starting point for a game.  With huge amounts of help from boyfriend, we got the game storing and retrieving those locations on AppEngine (again! start small! go slow!) And finally I modified index.html to figure out whether it's starting a pre-set game, a user-created game, or a random game.
It's just about ready to release now, I think, with one more fix: if someone calls a game that doesn't exist, the program needs to do something graceful.

Edit, a few hours later: got Israel, got a graceful error message to pop up.  The boyfriend also hacked the page and found he could store junk text in the datastore, but that didn't affect playability. He added code to address the vulnerability and so it seems more or less ready to roll now.

Monday, May 27, 2013

the world is large...

...and has a lot of very straight, very empty roads on it.

MapTag tips (which are really StreetView tips):

There is really no easy way to move a long distance in a single click using StreetView. The step distance seems to depend on how fast the car was going at the time.

The circle, which helps you take the largest step possible, is imperfect. You can't just leave the mouse on the same spot onscreen and keep clicking.  Eventually it will fuck up.  It seems if you move it a bit between each click, that will reduce the chances of it thinking you want to zoom, or adjust the view.  Occasionally I have seen it spin me 90-180 degrees off from where I was headed, so keep half an eye on your general heading in case that happens.

I find it rather interesting, after writing the game, that it's still fun to play.

At some point I may try to increase the chances of hitting urban/suburban areas.  Right now the valid random places are based on about 8 circles of varying sizes: Australia, New Zealand, South Africa, continental US/Canada/Mexico, Alaska/NW Canada, Hawaii, Europe, Japan, and Brazil.  Each has a probability, initially based on area size, but also hand-jiggered a tiny bit based on how much empty expanse it has.  I should tweak them more to have a stronger bias toward Europe, Japan, and such places that have less empty space.  I could specify urban areas (e.g. the US coasts) and give them a bit more probability of being selected.  Other places that should be added: Moscow, Israel, Thailand, Taiwan, Hong Kong, Singapore.

Sunday, May 26, 2013

finally wrote another game (but not Android)

Back in 2006 I wrote a Flash game, sort of an escaper.  In my hackathon fashion, I drew the scenes and worked out the puzzles first so I could download the 30-day Flash trial and learn enough about Flash to create the game before the trial ran out. I had it done in 20 days, a friend hosted it, and people liked it. But it was difficult enough that I didn't really want to do it again.

Fast foward to now, coming up on a year of Android coding under my belt, working with various Google APIs, along with a bit of JQuery and straight Java.  Things make a lot more sense now.  And I come across this neat little game called Pursued, which uses Google Streetview.  You get plopped into a location and you need to figure out what city you're in within a certain time limit.  It's got a lovely, slick HTML5 UI. You get achievement badges for going through themed sets of cities, and it has a separate section for players to add their own games.

Around the same time Yonatan Zunger posted about a game called Geoguessr.  Same general principle, but there's no time limit, and you're shooting for accuracy -- not a city name, but getting as close to the start point as possible.  The UI is less pretty but it's in many ways more challenging.

I mentioned Pursued to Yonatan, saying it would be nice to have a hybrid of the two games, with a slick interface and points for accuracy.  He agreed and suggested I write it.

Well, sometimes I laugh at suggestions.  Sometimes I take them as a challenge.

My all-things-coding tutor, aka boyfriend, has taught me one very important principle: start simple. Don't try to write the entire program at once.  So I got a div to display a navigable Streetview map. Then added a second large map that you could slide in and out from the side.  Then added the ability to drop a marker on that map, calculate where it is versus where the user is, and score it.  Then added another slide-out for an array of pre-set games. And finally some help and credits screens.

And this is all in Javascript, with a bunch of JQuery to listen for clicks and CSS to drive the animations.  Did I mention I don't really know much Javascript?

All in all, I wouldn't say it has a totally slick interface, but it has both a random-location game and a set of pre-set games based on a theme. Plus it scores you based on where you are in Streetview -- for some odd reason, Geoguessr scores your answer based on the original location it dropped you into, even though you might have had to travel for miles to figure out where you are.

I definitely needed help with the trigonometry of choosing a random location and measuring distance (I'm a little jealous that boyfriend didn't even need a refresher).  I received hints and general guidance, and occasionally outright coding. Apparently there are timing issues when you're asking someone else's server to give you large quantities of data: there are things you simply have to wait for, and you can't continue until you have that data in hand, so to speak. He taught me a bit about callbacks and (the formal term escapes me) chaining functions to guarantee one will start executing only after another is finished.  And of course he helped get my AppEngine settings set up so I could upload it there.

So without further ado: MapTag

Thursday, May 9, 2013

reading/writing Google Drive

Here are the broad steps to write a file from an Android device to Google Drive.  You create a unique SHA1 fingerprint for your application (step 1), tell Drive your application exists and will be utilising the Drive API (step 2).  The branding information is the "pretty" name for your app; the package name is its name in your code (e.g. com.example.myapp). Library and dependency installation (step 3) is easy if you have Eclipse; if you have IntelliJ, one of their programmers has shown how to create the analogous configuration on StackOverflow.

The sample code, Drive Quickstart, starts up the built-in camera app, grabs the Uri of the saved image, and uploads it to Drive.  In the process it needs to get a login name and authorization to write to Drive.  Since life is uncertain, it uses startActivityForResult, which returns a result code to determine the next action.  I needed significantly more complicated behavior for writing the file, but this is the basic sequence of events.


My application stores the user login (e.g. "mightyjoeyoung@gmail.com") in SharedPreferences, so once it's set, it won't ask again. There are some initial checks if a) a stored name exists and b) the stored name actually associates with a valid account. If it finds nothing, it starts an account picker Intent.


app  = (App) getApplication();
// check for a previously saved login
settings = getSharedPreferences(app.PREFS_NAME, 0);
String savedLogin = settings.getString("savedlogin", "");
if (!savedLogin.equals("")) {
    app.credential.setSelectedAccountName(savedLogin);
    service = getDriveService(app.credential);
}
if (savedInstanceState != null) {
    inProgress = savedInstanceState.getBoolean("inProgress");
}
if (app.credential.getSelectedAccount() == null) {
    startActivityForResult(app.credential.newChooseAccountIntent(), REQUEST_ACCOUNT_PICKER);
}


When a valid account is set, it authorizes and creates a Drive service object

Drive service = new Drive.Builder(AndroidHttp.newCompatibleTransport(), new GsonFactory(), credential).build();

which is used for the reading/writing transactions.  Failure to authorize results in going back to the account picker.

If all is well, it grabs the Uri of the file on the phone (which was created before starting the whole process) and attempts to upload it.

Drive Quickstart's sample code has no UI beyond the bare minimum.  While it's busy trying to upload, by all appearances the app has hung. And the first time it's run, it can take up to a minute to complete the process (or at least that's what happened to me).  They don't even bother to compensate for changes in screen orientation.  With a progress dialog popup and ongoing status messages, users will be less inclined to cancel.

Drive has a quirk (as mentioned in an earlier post) of keeping track of files with an internal ID rather than the file name. Multiple writes of the same file name results in multiple files with the same name.  Rather than confusing the user (and the macro that adds the records to the spreadsheet) I had to get hold of that file ID, save it to SharedPreferences, and when time came for rewriting, write to that ID.  And, if the user had happened to trash it, fish it out of the trash.

So the logic goes: if you have an ID previously saved in SharedPreferences, and that file exists, you're good to go and overwrite. (And fish it out of the trash, whether or not it's in trash, because it's not super easy to tell if it's in trash.)  If you don't have a saved ID, or you do but the file isn't there, you have to create a new file (and after it's written to Drive, get its file ID and save it).

Here is a list of the methods to work with Drive files. There is also some sample code at the end of each method's page.

One weirdness about the Drive Quickstart sample code: for reasons unknown, it asks for full access to all the files on the Drive.
credential = GoogleAccountCredential.usingOAuth2(this, DriveScopes.DRIVE);
It doesn't need this, and your users will be O_o if your app asks for the same permission. You really want your app to access only the files it creates:
credential = GoogleAccountCredential.usingOAuth2(this, DriveScopes.DRIVE_FILE);

Lastly, the boyfriend's phone seemed to need this added to the manifest:
<uses-permission android:name="android.permission.USE_CREDENTIALS"/>
I'm not sure if this was due to his Android version, or that he has more than one account registered on the phone, or that he has two-stage authentication.

Wednesday, May 1, 2013

Well, scratch that

I still don't know if I want to put it in the Play Store or not, but I restored (since that's what I had it doing before we decided to use Drive) a Share button.  That brings up the generic dialog to send a text file: your choice of Bluetooth, Copy to clipboard, Drive, Email, Gmail, Google, G+, and Text message. So it doesn't require Drive and the Drive spreadsheet macro, and the user is free to pop the data into their own spreadsheet or database.  It's just a CSV file.

And yes, I did make a steampunk scale. I photographed the top surface of a Deco vanity, mushed it into the right proportions, and stuck on Googled images of brass gears and wood borders and such.


However, There are semitransparent XML borders around the weight "dials" and the Save button (see previous post, updated to include the new Notes field and the changes mentioned above), and I haven't coded to compensate for the significantly darker color of the wood scale.The font color is that not-quite-black of Android text, and though the digits are nicely visible, Save is not.  It looks, in a word, awful.  So I'm not posting the pic.

The ideal thing would be to have a semitransparent white for the Save button and remove the border entirely for the dials. We'll just have to see how easy that is to do programmatically.

Wednesday, April 24, 2013

victory declared

Well, it seemed poised to get into the endless-polishing stage, but I think I'm pretty much done with the weight app.  (Although I would kind of like to draw a steampunk scale.)

This has pretty specific requirements. You need to have a Drive account and you need to have a spreadsheet with the macro installed.  It is also designed on the assumption that it is used to record a single weight at a time.  For these reasons I won't be putting this in the Play Store.

Main screen. It will remember the last
weight you entered.

Change background image

Of course, the enter date can be
changed anytime
List of weights and upload button.
Individual weights can be deleted by
swiping away. The menu option lets
you delete all the weights.


If you haven't signed in before, it will
bring up a picker to choose your
Google account. It will remember the
account for all subsequent uploads.







File is always uploaded to LatestWeights.txt on the root (this is not configurable).  The app will
always overwrite an existing file.  Technically speaking, you can use the text file simply as a way
of backing up the database on the phone.

The Google Spreadsheet uses a macro written to find LatestWeights.txt and add its records to the sheet.






























Monday, April 22, 2013

concerns about piracy

Update (5/10/13): Google took this pirate down and I feel gratified. He may just pop up somewhere else, but he'll have to pay the developer registration fee again.  Twenty-five dollars may not seem like much, but it offsets some of the ad revenue, which I've heard is pretty tiny.  Heck, my friend's book has been up for over 4 months and our combined profit hasn't hit that yet.

I was browsing Play for a game to download, when I came across this one. I thought it looked familiar, downloaded it and started playing.  Sure enough, it was this Flash game.

The Android version had a fair number of ads and started to annoy me, and I knew I'd played it before, so I deinstalled it and took a peek at this "developer's" other offerings.  There were a few Japanese games, a bunch of games in English.  All different styles of artwork. Then I came across one, then two, that had been written by a friend.

I had already seen this friend had ported a couple of his Flash games to Android under his own name.  This so-called "developer" was pirating other people's Flash games and posting them under his name, presumably for the ad revenue.

I looked up the Flash versions of several games and contacted four developers. One didn't respond, and the other three said they had not given permission to this guy to port their game.  I gave them the link to complain to Google about copyright violation. Hopefully this guy will get taken down.

But this leads me to ask what protections I have as a Play developer?  If someone installs my app, how easy would it be for them to take the app from their file system, load it into their IDE and add code to pull in ad networks, and push it back out to the Play store as their own?

Google recently won a case against Viacom wherein its subsidiary YouTube was found to be protected by the DMCA for hosting Viacom TV shows uploaded by its users.

Google also protects copyright in other cases, such as copyrighted music being used in user videos.  It has, as far as I understand, a fairly sophisticated algorithm to search for and match to copyrighted material.

Is the same effort put into protecting people who may not even know their work has been ported to Android?  Or at least work that already exists at the Play Store and is pirated into another Play Store app?  Or is it up to developers to constantly search for theft of their own work?

Wednesday, April 10, 2013

The weight project part 2

Eclipse vs IntelliJ and The Google (Drive)

I started programming Java, then Android with IntelliJ some 10 months ago.  I like IntelliJ.  However, the Google Drive example assumed you were using Eclipse, a popular and also free IDE.  It has its own plugin to incorporate Google libraries.  I admit I don't understand more than the rudiments of code libraries, so when I started working on the example I could not figure out how to make it work in IntelliJ.

I downloaded Eclipse, and after some struggle with the newest version and learning that it and the Android SDK don't play all so well together, downloaded the next-older version and got it working.

I also learned enough about Eclipse to realize I didn't like it all that much.  It's slower, even when you tell it not to automatically compile, and seems delicate.  It keeps track of projects in a completely separate folder, so if you delete a file from outside, or rename/move the folder, then try to load/run it, Eclipse has a hissy fit and faints.  IntelliJ just looks at what is there and deals with it, even if your project is open.  Robust.

I struggled for days trying to add the libraries to IntelliJ the way I was guessing they were being added in Eclipse.  (On the Drive example page, there was no description of what the plugin was doing under the hood.)  I finally broke down and asked the collective wisdom of StackOverflow.

After some wrangling with a guy who initially didn't seem to understand what I was asking, he built the example himself and presented me with exactly how to add the libraries. Bless this sweet Russian guy's heart.  Once I had this, I placed it straight into the weights app and it worked beautifully. Yay!

Saturday, April 6, 2013

tackling Google Drive

The boyfriend steps on the scale every day, records his weight on the whiteboard, and every once in a while enters those into a Drive spreadsheet, which has a little chart showing his progress.  (At 177 +-2, he's quite reasonable, but he feels tracking helps him keep it that way.) So I wrote a little database app for his phone.  It saves weights to a comma delimited text file and uploads it to Drive.  A macro in the spreadsheet grabs that file and puts the entries at the end of the sheet.

The fun (/sarcasm) part has been learning to upload or update this file on Drive.  The API is less than well documented, a lot of the code is compiled so you can't read it in the IDE, and that makes it extra painful since I'm barely versed in these sorts of things.  However, there's some nice sample text at the Developers site that show the basics in Java (see "Manage Drive Files").

Interestingly, Drive commands are not so much like a file system as a database.  One updates and inserts files instead of copying them.  Drive files have an ID, which is a separate creature from the name ("title").  Inserting a file will create a new file, and it could have the same title as another file that already exists -- it won't automatically overwrite it.  It's got a different ID, so it must be a different file.  So you have to look for the file and grab its ID, and from there you can update it, or do other stuff like delete, change its title, etc.

Drive requests do not ignore the Trash folder, either.  During testing, I deleted the uploaded file from Drive, expecting the upload to create a new one, and was frustrated for quite some time because my "new" file wasn't showing up. It had been happily updating the deleted file.

The other thing to watch out for is how much you're trying to do.  I followed the Developers sample code to list the files, and it grabs *all* the files, Trash, subfolders, everything. You have to add a query (yeah, database again) to restrict the file listing to where you want it to look, and better yet, the name.  After adding the query

request.setQ("'root' in parents and title = 'LatestWeights.txt' and trashed=false");

the whole process speeded up remarkably.