Merge pull request #201 from coreos/godep-to-glide

vendor: Switch from godep to glide tool
This commit is contained in:
Dalton Hubble
2016-05-13 11:18:05 -07:00
3185 changed files with 633008 additions and 25383 deletions

View File

@@ -23,7 +23,7 @@ Alternately, build a Docker image `coreos/bootcfg:latest`.
sudo ./build-docker
## Check Version
## Version
./bin/bootcfg -version
sudo rkt --insecure-options=image run bootcfg.aci -- -version
@@ -41,4 +41,19 @@ Run the ACI with rkt on `metal0`.
Alternately, run the Docker image on `docker0`.
sudo docker run -p 8080:8080 --rm -v $PWD/examples:/var/lib/bootcfg:Z -v $PWD/examples/groups/etcd-docker:/var/lib/bootcfg/groups:Z coreos/bootcfg:latest -address=0.0.0.0:8080 -log-level=debug
sudo docker run -p 8080:8080 --rm -v $PWD/examples:/var/lib/bootcfg:Z -v $PWD/examples/groups/etcd-docker:/var/lib/bootcfg/groups:Z coreos/bootcfg:latest -address=0.0.0.0:8080 -log-level=debug
## Dependencies
Project dependencies are commited to the `vendor` directory, so Go 1.6+ users can clone to their `GOPATH` and build or test immediately. Go 1.5 users should set `GO15VENDOREXPERIMENT=1`.
Project developers should use [glide](https://github.com/Masterminds/glide) to manage commited dependencies under `vendor`. Configure `glide.yaml` as desired. Use `glide update` to download and update dependencies listed in `glide.yaml` into `/vendor` (do **not** use glide `get`).
glide update --update-vendored --strip-vendor --strip-vcs
Recursive dependencies are also vendored. A `glide.lock` will be created to represent the exact versions of each dependency.
With an empty `vendor` directory, you can install the `glide.lock` dependencies.
rm -rf vendor/
glide install --strip-vendor --strip-vcs

95
glide.lock generated Normal file
View File

@@ -0,0 +1,95 @@
hash: 8f33fd1c87e2136cdff69364e0668a595a129b8ffd686851336c841bf7e4f705
updated: 2016-05-12T14:04:55.653773498-07:00
imports:
- name: github.com/alecthomas/units
version: 2efee857e7cfd4f3d0138cc3cbb1b4966962b93a
- name: github.com/camlistore/camlistore
version: 9106ce829629773474c689b34aacd7d3aaa99426
subpackages:
- pkg/errorutil
- name: github.com/coreos/coreos-cloudinit
version: b3f805dee6a4aa5ed298a1f370284df470eecf43
subpackages:
- config
- name: github.com/coreos/go-semver
version: 294930c1e79c64e7dbe360054274fdad492c8cf5
subpackages:
- semver
- name: github.com/coreos/go-systemd
version: 7b2428fec40033549c68f54e26e89e7ca9a9ce31
subpackages:
- journal
- name: github.com/coreos/ignition
version: 44c274ab414294a8e34b3a940e0ec1afe6b6c610
subpackages:
- config
- config/types
- config/v1
- config/v1/types
- name: github.com/coreos/pkg
version: 66fe44ad037ccb80329115cb4db0dbe8e9beb03a
subpackages:
- capnslog
- flagutil
- name: github.com/coreos/yaml
version: 6b16a5714269b2f70720a45406b1babd947a17ef
- name: github.com/davecgh/go-spew
version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d
subpackages:
- spew
- name: github.com/golang/protobuf
version: f0a097ddac24fb00e07d2ac17f8671423f3ea47c
subpackages:
- proto
- name: github.com/inconshreveable/mousetrap
version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
- name: github.com/pmezard/go-difflib
version: 792786c7400a136282c1664665ae0a8db921c6c2
subpackages:
- difflib
- name: github.com/spf13/cobra
version: 65a708cee0a4424f4e353d031ce440643e312f92
- name: github.com/spf13/pflag
version: 7f60f83a2c81bc3c3c0d5297f61ddfa68da9d3b7
- name: github.com/stretchr/testify
version: 1f4a1643a57e798696635ea4c126e9127adb7d3c
subpackages:
- assert
- name: github.com/vincent-petithory/dataurl
version: 9a301d65acbb728fcc3ace14f45f511a4cfeea9c
- name: go4.org
version: 03efcb870d84809319ea509714dd6d19a1498483
subpackages:
- errorutil
- name: golang.org/x/crypto
version: 5dc8cb4b8a8eb076cbb5a06bc3b8682c15bdbbd3
subpackages:
- cast5
- openpgp
- openpgp/armor
- openpgp/errors
- openpgp/packet
- openpgp/s2k
- openpgp/elgamal
- name: golang.org/x/net
version: fb93926129b8ec0056f2f458b1f519654814edf0
subpackages:
- context
- http2
- internal/timeseries
- trace
- http2/hpack
- name: google.golang.org/grpc
version: 8eeecf2291de9d171d0b1392a27ff3975679f4f5
subpackages:
- codes
- credentials
- grpclog
- internal
- metadata
- naming
- transport
- peer
- name: gopkg.in/yaml.v2
version: f7716cbe52baa25d2e9b0d0da546fcf909fc16b4
devImports: []

77
glide.yaml Normal file
View File

@@ -0,0 +1,77 @@
package: github.com/coreos/coreos-baremetal
import:
- package: github.com/alecthomas/units
version: 2efee857e7cfd4f3d0138cc3cbb1b4966962b93a
- package: github.com/camlistore/camlistore
version: 9106ce829629773474c689b34aacd7d3aaa99426
- package: github.com/coreos/coreos-cloudinit
version: b3f805dee6a4aa5ed298a1f370284df470eecf43
subpackages:
- Godeps/_workspace/src/github.com/coreos/yaml
- config
- package: github.com/coreos/go-semver
version: 294930c1e79c64e7dbe360054274fdad492c8cf5
subpackages:
- semver
- package: github.com/coreos/go-systemd
version: 7b2428fec40033549c68f54e26e89e7ca9a9ce31
subpackages:
- journal
- package: github.com/coreos/ignition
version: 44c274ab414294a8e34b3a940e0ec1afe6b6c610
subpackages:
- config
- config/types
- config/v1
- config/v1/types
- package: github.com/coreos/pkg
version: 66fe44ad037ccb80329115cb4db0dbe8e9beb03a
subpackages:
- capnslog
- flagutil
- package: github.com/davecgh/go-spew
version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d
subpackages:
- spew
- package: github.com/golang/protobuf
version: f0a097ddac24fb00e07d2ac17f8671423f3ea47c
subpackages:
- proto
- package: github.com/pmezard/go-difflib
version: 792786c7400a136282c1664665ae0a8db921c6c2
subpackages:
- difflib
- package: github.com/spf13/cobra
version: 65a708cee0a4424f4e353d031ce440643e312f92
- package: github.com/spf13/pflag
version: 7f60f83a2c81bc3c3c0d5297f61ddfa68da9d3b7
- package: github.com/stretchr/testify
version: 1f4a1643a57e798696635ea4c126e9127adb7d3c
subpackages:
- assert
- package: github.com/vincent-petithory/dataurl
version: 9a301d65acbb728fcc3ace14f45f511a4cfeea9c
- package: go4.org
version: 03efcb870d84809319ea509714dd6d19a1498483
subpackages:
- errorutil
- package: golang.org/x/crypto
version: 5dc8cb4b8a8eb076cbb5a06bc3b8682c15bdbbd3
subpackages:
- cast5
- openpgp
- package: golang.org/x/net
version: fb93926129b8ec0056f2f458b1f519654814edf0
subpackages:
- context
- http2
- internal/timeseries
- trace
- package: google.golang.org/grpc
version: 8eeecf2291de9d171d0b1392a27ff3975679f4f5
subpackages:
- codes
- package: gopkg.in/yaml.v2
version: f7716cbe52baa25d2e9b0d0da546fcf909fc16b4
- package: github.com/coreos/yaml
version: 6b16a5714269b2f70720a45406b1babd947a17ef

34
vendor/github.com/camlistore/camlistore/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,34 @@
*~
*.o
*.pyc
\#*\#
.\#*
.*.swp
logs
_obj
[68].out
_test
_gotest*
_testmain*
_go_.[568]
_cgo*
clients/go/camgsinit/camgsinit
clients/go/camwebdav/camwebdav
.goroot
appengine-sdk
build/root
.DS_Store
bin/cam*
bin/devcam
bin/*_*
bin/hello
bin/publisher
tmp
server/camlistored/newui/all.js
server/camlistored/newui/all.js.map
server/camlistored/newui/zembed_all.js.go
server/appengine/source_root/
config/tls.*
misc/docker/djpeg-static/djpeg
misc/docker/camlistored/camlistored*
misc/docker/camlistored/djpeg

View File

@@ -0,0 +1,23 @@
Brett and I eating Burritos from Little Chihuahua at my house.
Plan is for Brett to work on Clip-it-Good (the Chrome Extension)'s
camli support (currently non-existent), and get it to:
-- select an image
-- upload the image blob
-- create the permanode blob
-- create (and sign, with the signing server) the "become" claim,
pointing the permanode at the image
-- create (a signed) "tag" blob, tagging an image e.g. "funny"
I will work on docs & signing server tests & signing verification
endpoint.
--------------
Done:
* Brad: docs re-organized
* Brad: camlistore.{com,org,net,info,us} domains purchased

View File

@@ -0,0 +1,4 @@
Saturday & Sunday in Paris with Mathieu, meeting for the first time,
hacking on EXIF rotation, thumbnail indexing, Postgres support, and
then Monday at Google Paris, working on different parts of the UI
permanode thumbnail page, and genfileembed problems.

View File

@@ -0,0 +1 @@
Closure newui hacking with bslatkin.

View File

@@ -0,0 +1,5 @@
At Brett's place, with Brett Slatkin, Lindsey Simon, Ryan Barrett.
Goal: More closure UI stuff.
Ryan getting up-to-speed and maybe working on Activity Streams import.

View File

@@ -0,0 +1,8 @@
Aaron Boodman, react js permanode UI
Andy Smith, discussing data model, updating HACKING, etc
Brad Fitzpatrick, misc
Brett Slatkin, web screenshotting client app, claim creation API/ACLs
Daisy Stanton, her own thing
Dan Erat, music player app
Emil Eklund, getting up to speed, photo stuff
Nick O'Neill, iOS

20
vendor/github.com/camlistore/camlistore/.header generated vendored Normal file
View File

@@ -0,0 +1,20 @@
/*
Copyright 2015 The Camlistore Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package x
import (
)

65
vendor/github.com/camlistore/camlistore/AUTHORS generated vendored Normal file
View File

@@ -0,0 +1,65 @@
# This is the official list of Camlistore authors for copyright purposes.
# This file is distinct from the CONTRIBUTORS files.
# See the latter for an explanation.
Aaron Bieber <deftly@gmail.com>
Alessandro Arzilli <alessandro.arzilli@gmail.com> gh=aarzilli
Amir Mohammad Saied <amirsaied@gmail.com>
Amit Levy <aalevy@gmail.com>
Andy Smith <github@anarkystic.com>
Anthony Martin <ality@pbrane.org>
Antonin Amand <antonin.amand@gmail.com>
Antti Rasinen <ars@iki.fi>
Armen Baghumian <armen@vardump.org>
Bret Comnes <bcomnes@gmail.com>
Brian Marete <marete@toshnix.com>
Caine Tighe <arctanofyourface@gmail.com>
Dan Kortschak <dan.kortschak@adelaide.edu.au>
Daniel Coonce <danielcoonce@gmail.com>
Daniel Dermott Bryan <danbryan@gmail.com>
Daniel Pupius <dan.pupius@gmail.com>
Dean Landolt <dean@deanlandolt.com>
Dustin Sallings <dsallings@gmail.com>
Edward Sheffler III <ed@flin.ch>
Emil Hessman <emil@hessman.se>
Eric Drechsel <eric@pdxhub.org>
Fabian Wickborn <fabian@wickborn.net>
Gina White <ginabythebay@gmail.com>
Google Inc.
Govert Versluis <govert@ver.slu.is>
Hernan Grecco <hernan.grecco@gmail.com>
Iain Peet <iain.peet@gmail.com> <iain@ipeet.org>
Jakub Brzeski <jk.brzeski@gmail.com>
Jani Monoses <jani@ubuntu.com>
Jingguo Yao <yaojingguo@gmail.com>
Josh Bleecher Snyder <josharian@gmail.com>
Josh Huckabee <joshhuckabee@gmail.com>
Joshua Gay <joshuagay@gmail.com>
Jrabbit <jackjrabbit@gmail.com>
Julien Danjou <julien@danjou.info>
Kamil Kisiel <kamil@kamilkisiel.net> <kamil.kisiel@gmail.com>
Kristopher Cost <me@krisco.st>
Lindsey Simon <lsimon@commoner.com>
Mario Russo <mail.mr@gmail.com>
Mateus Braga <mateus.a.braga@gmail.com>
Mathieu Lonjaret <mathieu.lonjaret@gmail.com>
Matthieu Rakotojaona Rainimangavelo <matthieu.rakotojaona@gmail.com>
Matt Jibson <matt.jibson@gmail.com>
Maxime Lavigne <duguigne@gmail.com>
Michael Vincent Zuffoletti <mikezuff@gmail.com>
Nick O'Neill <nick.oneill@gmail.com>
Nolan Darilek <nolan@thewordnerd.info>
Philio <phil@bayfmail.com>
Piotr Staszewski <p.staszewski@gmail.com>
Ranveer <ranveerkunal@gmail.com>
Ritesh Sinha <ritesh.kumar.sinha@gmail.com>
Rob Young <bubblenut@gmail.com>
Robert Obryk <robryk@gmail.com>
Robert Hencke <robert.hencke@gmail.com>
Salman Aljammaz <s@0x65.net>
Sarath Lakshman <sarathlakshman@slynux.com>
Steve Phillips <elimisteve@gmail.com>
Steven L. Speek <slspeek@gmail.com>
Tamás Gulácsi <tgulacsi78@gmail.com>
Timo Truyts <alkaloid.btx@gmail.com>
Ulf Holm Nielsen <doktor@dyregod.dk>

12
vendor/github.com/camlistore/camlistore/BUILDING generated vendored Normal file
View File

@@ -0,0 +1,12 @@
To build Camlistore:
1) Install Go 1.5 or later.
2) cd to the root of the Camlistore source (where this file is)
3) Run:
$ go run make.go
4) The compiled binaries should now be in the "bin" subdirectory:
camlistored (the server), camget, camput, and camtool.

93
vendor/github.com/camlistore/camlistore/CONTRIBUTORS generated vendored Normal file
View File

@@ -0,0 +1,93 @@
# People who have agreed to one of the CLAs and can contribute patches.
# The AUTHORS file lists the copyright holders; this file
# lists people. For example, Google employees are listed here
# but not in AUTHORS, because Google holds the copyright.
#
# http://code.google.com/legal/individual-cla-v1.0.html (electronic submission)
# http://code.google.com/legal/corporate-cla-v1.0.html (requires FAX)
#
# Note that the CLA isn't a copyright _assignment_ but rather a
# copyright _license_. You retain the copyright on your
# contributions.
Aaron Bieber <deftly@gmail.com>
Aaron Boodman <aaron@aaronboodman.com>
Aaron Racine <aracine@google.com>
Adam Langley <agl@golang.org>
Alessandro Arzilli <alessandro.arzilli@gmail.com> gh=aarzilli
Ali Afshar <afshar@google.com>
Amir Mohammad Saied <amirsaied@gmail.com>
Amit Levy <aalevy@gmail.com>
Andrew Gerrand <adg@golang.org>
Andy Smith <github@anarkystic.com>
Anthony Martin <ality@pbrane.org>
Antonin Amand <antonin.amand@gmail.com>
Antti Rasinen <ars@iki.fi>
Armen Baghumian <armen@vardump.org>
Bill Thiede <couchmoney@gmail.com>
Brad Fitzpatrick <brad@danga.com>
Bret Comnes <bcomnes@gmail.com>
Brett Slatkin <bslatkin@gmail.com>
Brian Marete <marete@toshnix.com>
Burcu Dogan <jbd@google.com>
Caine Tighe <arctanofyourface@gmail.com>
Dan Kortschak <dan.kortschak@adelaide.edu.au>
Daniel Coonce <danielcoonce@gmail.com>
Daniel Dermott Bryan <danbryan@gmail.com>
Daniel Erat <dan@erat.org>
Daniel Pupius <dan.pupius@gmail.com>
Dean Landolt <dean@deanlandolt.com>
Dustin Sallings <dsallings@gmail.com>
Edward Sheffler III <ed@flin.ch>
Emil Hessman <emil@hessman.se>
Eric Drechsel <eric@pdxhub.org>
Evan Martin <martine@danga.com>
Fabian Wickborn <fabian@wickborn.net>
Gina White <ginabythebay@gmail.com>
Govert Versluis <govert@ver.slu.is>
Han-Wen Nienhuys <hanwen@google.com>
Hernan Grecco <hernan.grecco@gmail.com>
Iain Peet <iain.peet@gmail.com> <iain@ipeet.org>
Jakub Brzeski <jk.brzeski@gmail.com>
Jani Monoses <jani@ubuntu.com>
Jingguo Yao <yaojingguo@gmail.com>
Johan Euphrosine <proppy@google.com>
Josh Bleecher Snyder <josharian@gmail.com>
Josh Huckabee <joshhuckabee@gmail.com>
Joshua Gay <joshuagay@gmail.com>
Jrabbit <jackjrabbit@gmail.com>
Julien Danjou <julien@danjou.info>
Kamil Kisiel <kamil@kamilkisiel.net> <kamil.kisiel@gmail.com>
Kristopher Cost <me@krisco.st>
Lindsey Simon <lsimon@commoner.com>
Marc-Antoine Ruel <maruel@chromium.org>
Mario Russo <mail.mr@gmail.com>
Mateus Braga <mateus.a.braga@gmail.com>
Mathieu Lonjaret <mathieu.lonjaret@gmail.com>
Matthieu Rakotojaona Rainimangavelo <matthieu.rakotojaona@gmail.com>
Matt Jibson <matt.jibson@gmail.com>
Maxime Lavigne <duguigne@gmail.com>
Michael Vincent Zuffoletti <mikezuff@gmail.com>
Nick O'Neill <nick.oneill@gmail.com>
Nico Weber <thakis@chromium.org>
Nigel Tao <nigeltao@golang.org>
Nolan Darilek <nolan@thewordnerd.info>
Pawel Szczur <pawelszczur@gmail.com>
Philio <phil@bayfmail.com>
Piotr Staszewski <p.staszewski@gmail.com>
Ranveer <ranveerkunal@gmail.com>
Ritesh Sinha <ritesh.kumar.sinha@gmail.com>
Rob Young <bubblenut@gmail.com>
Robert Hencke <robert.hencke@gmail.com>
Robert Kroeger <rjkroege@liqui.org>
Robert Obryk <robryk@gmail.com>
Ryan Barrett <camlistore@ryanb.org>
Salman Aljammaz <s@0x65.net>
Sarath Lakshman <sarathlakshman@slynux.com>
Steve Phillips <elimisteve@gmail.com>
Steven L. Speek <slspeek@gmail.com>
Tamás Gulácsi <tgulacsi78@gmail.com>
Timo Truyts <alkaloid.btx@gmail.com>
Tony Chang <tony@ponderer.org>
Tony Scelfo <scelfo@tonyscelfo.com>
Ulf Holm Nielsen <doktor@dyregod.dk>

56
vendor/github.com/camlistore/camlistore/Dockerfile generated vendored Normal file
View File

@@ -0,0 +1,56 @@
# Build everything at least. This is a work in progress.
#
# Useful for testing things before a release.
#
# Will also be used for running the camlistore.org website and public
# read-only blobserver.
FROM ubuntu:12.04
MAINTAINER camlistore <camlistore@googlegroups.com>
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get upgrade -y
RUN apt-get install -y curl make git
RUN curl -o /tmp/go.tar.gz https://storage.googleapis.com/golang/go1.3.1.linux-amd64.tar.gz
RUN tar -C /usr/local -zxvf /tmp/go.tar.gz
RUN rm /tmp/go.tar.gz
RUN /usr/local/go/bin/go version
ENV GOROOT /usr/local/go
ENV PATH $GOROOT/bin:/gopath/bin:$PATH
RUN mkdir -p /gopath/src
ADD pkg /gopath/src/camlistore.org/pkg
ADD cmd /gopath/src/camlistore.org/cmd
ADD website /gopath/src/camlistore.org/website
ADD third_party /gopath/src/camlistore.org/third_party
ADD server /gopath/src/camlistore.org/server
ADD dev /gopath/src/camlistore.org/dev
ADD depcheck /gopath/src/camlistore.org/depcheck
RUN adduser --disabled-password --quiet --gecos Camli camli
RUN mkdir -p /gopath/bin
RUN chown camli.camli /gopath/bin
RUN mkdir -p /gopath/pkg
RUN chown camli.camli /gopath/pkg
USER camli
ENV GOPATH /gopath
RUN go install --tags=purego \
camlistore.org/server/camlistored \
camlistore.org/cmd/camput \
camlistore.org/cmd/camget \
camlistore.org/cmd/camtool \
camlistore.org/website \
camlistore.org/dev/devcam
ENV USER camli
ENV HOME /home/camli
WORKDIR /home/camli
EXPOSE 80 443 3179 8080
CMD /bin/bash

110
vendor/github.com/camlistore/camlistore/HACKING generated vendored Normal file
View File

@@ -0,0 +1,110 @@
Camlistore contributors regularly use Linux and OS X, and both are
100% supported.
Developing on Windows is sometimes broken, but should work. Let us
know if we broke something, or we accidentally depend on some
Unix-specific build tool somewhere.
See http://camlistore.org/docs/contributing for information on how to
contribute to the project and submit patches. Notably, we use Gerrit
for code review. Our Gerrit instance is at https://camlistore.org/r/
See architecture docs: https://camlistore.org/docs/
You can view docs for Camlistore packages with local godoc, or
godoc.org.
It's recommended you use git to fetch the source code, rather than
hack from a Camlistore release's zip file:
$ git clone https://camlistore.googlesource.com/camlistore
(We use github for distribution but its code review system is so poor,
we don't use its Pull Request mechanism. The Gerrit git server & code
review system is the main repo. See
http://camlistore.org/docs/contributing for how to use them. We might
support github for pull requests in the future, once it's properly
integrated with external code review tools. We had a meeting with Github
to discuss the ways in which their code review tools are poor.)
On Debian/Ubuntu, some deps to get started:
$ sudo apt-get install libsqlite3-dev sqlite3 pkg-config git
During development, rather than use the main binaries ("camput",
"camget", "camtool", "cammount", etc) directly, we instead use a
wrapper (devcam) that automatically configure the environment to use
the test server & test environment.
To build devcam:
$ go run make.go
And devcam will be in <camroot>/bin/devcam. You'll probably want to
symlink it into your $PATH.
Alternatively, if your Camlistore root is checked out at
$GOPATH/src/camlistore.org (optional, but natural for Go users), you
can just:
$ export GO15VENDOREXPERIMENT=1 # required for all Camlistore builds
$ go install ./dev/devcam
The subcommands of devcam start the server or run camput/camget/etc:
$ devcam server # main server
$ devcam appengine # App Engine version of the server
$ devcam put # camput
$ devcam get # camget
$ devcam tool # camtool
$ devcam mount # cammount
Once the dev server is running,
- Upload a file:
devcam put file ~/camlistore/COPYING
- Create a permanode:
devcam put permanode
- Use the UI: http://localhost:3179/ui/
Before submitting a patch, you should check that all the tests pass with:
$ devcam test
You can use your usual git workflow to commit your changes, but for each
change to be reviewed you should merge your commits into one before submitting
to gerrit for review.
You should also try to write a meaningful commit message, which at least states
in the first sentence what part or package of camlistore this commit is affecting.
The following text should state what problem the change is addressing, and how.
Finally, you should refer to the github issue(s) the commit is addressing, if any,
and with the appropriate keyword if the commit is fixing the issue. (See
https://help.github.com/articles/closing-issues-via-commit-messages/).
For example:
"
pkg/search: add "file" predicate to search by file name
File names were already indexed but there was no way to query the index for a file
by its name. The "file" predicate can now be used in search expressions (e.g. in the
search box of the web user interface) to achieve that.
Fixes #10987
"
If your commit is adding or updating a vendored third party, you must indicate
in your commit message the version (e.g. git commit hash) of said third party.
You can optionally use our pre-commit hook so that your code gets gofmt'ed
before being submitted (which should be done anyway).
$ cd .git/hooks
$ ln -s ../../misc/pre-commit.githook pre-commit
Finally, submit your code to gerrit with:
$ devcam review
Please update this file as appropriate.

36
vendor/github.com/camlistore/camlistore/Makefile generated vendored Normal file
View File

@@ -0,0 +1,36 @@
# The normal way to build Camlistore is just "go run make.go", which
# works everywhere, even on systems without Make. The rest of this
# Makefile is mostly historical and should hopefully disappear over
# time.
all:
go run make.go
# On OS X with "brew install sqlite3", you need PKG_CONFIG_PATH=/usr/local/Cellar/sqlite/3.7.17/lib/pkgconfig/
full:
go install --ldflags="-X camlistore.org/pkg/buildinfo.GitInfo "`./misc/gitversion` `pkg-config --libs sqlite3 1>/dev/null 2>/dev/null && echo "--tags=with_sqlite"` ./pkg/... ./server/... ./cmd/... ./third_party/... ./dev/...
# Workaround Go bug where the $GOPATH/pkg cache doesn't know about tag changes.
# Useful when you accidentally run "make" and then "make presubmit" doesn't work.
# See https://code.google.com/p/go/issues/detail?id=4443
forcefull:
go install -a --tags=with_sqlite ./pkg/... ./server/camlistored ./cmd/... ./dev/...
oldpresubmit: fmt
SKIP_DEP_TESTS=1 go test `pkg-config --libs sqlite3 1>/dev/null 2>/dev/null && echo "--tags=with_sqlite"` -short ./pkg/... ./server/camlistored/... ./server/appengine ./cmd/... ./dev/... && echo PASS
presubmit: fmt
go run dev/devcam/*.go test -short
embeds:
go install ./pkg/fileembed/genfileembed/ && genfileembed ./server/camlistored/ui && genfileembed ./pkg/server
UIDIR = server/camlistored/ui
NEWUIDIR = server/camlistored/newui
clean:
rm -f $(NEWUIDIR)/all.js $(NEWUIDIR)/all.js.map
fmt:
go fmt camlistore.org/cmd... camlistore.org/dev... camlistore.org/misc... camlistore.org/pkg... camlistore.org/server...

23
vendor/github.com/camlistore/camlistore/README generated vendored Normal file
View File

@@ -0,0 +1,23 @@
Camlistore is your personal storage system for life.
It's a way to store, sync, share, model and back up content.
It stands for "Content-Addressable Multi-Layer Indexed Storage", for
lack of a better name. For more, see:
http://camlistore.org/
http://camlistore.org/docs/
Other useful files:
BUILDING how to compile it ("go run make.go")
HACKING how to do development and contribute
Mailing lists:
http://camlistore.org/lists
Bugs and contributing:
https://github.com/camlistore/camlistore/issues
http://camlistore.org/docs/contributing

34
vendor/github.com/camlistore/camlistore/TESTS generated vendored Normal file
View File

@@ -0,0 +1,34 @@
Tests needed
-- integration test of reindexing + race detector
-- support for running race detector on all tests. when in race mode, also run
integration test children in race mode.
-- test that server/camlistored still builds & starts even when sqlite isn't
available (TODO: hide it from the test by running make.go in a child
process with a faked-out PKG_CONFIG environment or something, to make
cmd/go unable to find it even if it's installed)
-- search & corpus use of EnumeratePermanodesLastModified
-- pkg/client --- test FetchVia against a server returning compressed content.
(fix in 3fa6d69405f036308931dd36e5070b2b19dbeadf without a new test)
-cmd/camput/
-verify that stat caching works. verify that -filenodes does create the permanode even if the file was already uploaded (and cached) in a previous run.
-- blobserver/{remote,shard} have no tests. should be easier now that
test.Fetcher is a full blobserver? see encrypt, replica, and cond's
nascent tests for examples.
-- app engine integration tests (before we make a release, for sure,
but probably in presubmit)
-- cross-compiling to freebsd and windows etc still works.
-- pkg/auth -- not enough tests. see regression at
https://camlistore-review.googlesource.com/#/c/556/1
-- blobserver.WaitForBlob, and integration tests for the http handlers
for long-polling on Enumerate and Stat

253
vendor/github.com/camlistore/camlistore/TODO generated vendored Normal file
View File

@@ -0,0 +1,253 @@
There are two TODO lists. This file (good for airplanes) and the online bug tracker:
https://github.com/camlistore/camlistore/issues
Offline list:
-- fix the presubmit's gofmt to be happy about emacs:
go fmt camlistore.org/cmd... camlistore.org/dev... camlistore.org/misc... camlistore.org/pkg... camlistore.org/server...
stat pkg/blobserver/.#multistream_test.go: no such file or directory
exit status 2
make: *** [fmt] Error 1
-- add HTTP handler for blobstreamer. stream a tar file? where to put
continuation token? special file after each tar entry? special file
at the end? HTTP Trailers? (but nobody supports them)
-- reindexing:
* add streaming interface to localdisk? maybe, even though not ideal, but
really: migrate my personal instance from localdisk to blobpacked +
maybe diskpacked for loose blobs? start by migrating to blobpacked and
measuring size of loose.
* add blobserver.EnumerateAllUnsorted (which could use StreamBlobs
if available, else use EnumerateAll, else maybe even use a new
interface method that goes forever and can't resume at a point,
but can be canceled, and localdisk could implement that at least)
* add buffered sorted.KeyValue implementation: a memory one (of
configurable max size) in front of a real disk one. add a Flush method
to it. also Flush when memory gets big enough.
In progress: pkg/sorted/buffer
-- stop using the "cond" blob router storage type in genconfig, as
well as the /bs-and-index/ "replica" storage type, and just let the
index register its own AddReceiveHook like the sync handler
(pkg/server/sync.go). But whereas the sync handler only synchronously
_enqueues_ the blob to replicate, the indexer should synchronously
do the ReceiveBlob (ooo-reindex) on it too before returning.
But the sync handler, despite technically only synchronously-enqueueing
and being therefore async, is still very fast. It's likely the
sync handler will therefore send a ReceiveBlob to the indexer
at the ~same time the indexer is already indexing it. So the indexer
should have some dup/merge suppression, and not do double work.
singleflight should work. The loser should still consume the
source io.Reader body and reply with the same error value.
-- ditch the importer.Interrupt type and pass along a context.Context
instead, which has its Done channel for cancelation.
-- be able to put a search (expr or JSON) into camlistore as a blob,
and search on it. and then name it with a permanode, and then
use a expr search like "named:someset" which looks up someset's
current camliContent, fetches it, and then expands into that blob's
search.expr or search.Constraint.
-- S3-only mode doesn't work with a local disk index (kvfile) because
there's no directory for us to put the kv in.
-- fault injection many more places with pkg/fault. maybe even in all
handlers automatically somehow?
-- sync handler's shard validation doesn't retry on error.
only reports the errors now.
-- export blobserver.checkHashReader and document it with
the blob.Fetcher docs.
-- "filestogether" handler, putting related blobs (e.g. files)
next to each other in bigger blobs / separate files, and recording
offsets of small blobs into bigger ones
-- diskpacked doesn't seem to sync its index quickly enough.
A new blob receieved + process exit + read in a new process
doesn't find that blob. kv bug? Seems to need an explicit Close.
This feels broken. Add tests & debug.
-- websocket upload protocol. different write & read on same socket,
as opposed to HTTP, to have multiple chunks in flight.
-- extension to blobserver upload protocol to minimize fsyncs: maybe a
client can say "no rush" on a bunch of data blobs first (which
still don't get acked back over websocket until they've been
fsynced), and then when the client uploads the schema/vivivy blob,
that websocket message won't have the "no rush" flag, calling the
optional blobserver.Storage method to fsync (in the case of
diskpacked/localdisk) and getting all the "uploaded" messages back
for the data chunks that were written-but-not-synced.
-- measure FUSE operations, latency, round-trips, performance.
see next item:
-- ... we probaby need a "describe all chunks in file" HTTP handler.
then FUSE (when it sees sequential access) can say "what's the
list of all chunks in this file?" and then fetch them all at once.
see next item:
-- ... HTTP handler to get multiple blobs at once. multi-download
in multipart/mime body. we have this for stat and upload, but
not download.
-- ... if we do blob fetching over websocket too, then we can support
cancellation of blob requests. Then we can combine the previous
two items: FUSE client can ask the server, over websockets, for a
list of all chunks, and to also start streaming them all. assume a
high-latency (but acceptable bandwidth) link. the chunks are
already in flight, but some might be redundant. once the client figures
out some might be redundant, it can issue "stop send" messages over
that websocket connection to prevent dups. this should work on
both "files" and "bytes" types.
-- cacher: configurable policy on max cache size. clean oldest
things (consider mtime+atime) to get back under max cache size.
maybe prefer keeping small things (metadata blobs) too,
and only delete large data chunks.
-- UI: video, at least thumbnailing (use external program,
like VLC or whatever nautilus uses?)
-- rename server.ImageHandler to ThumbnailRequest or something? It's
not really a Handler in the normal sense. It's not built once and
called repeatedly; it's built for every ServeHTTP request.
-- unexport more stuff from pkg/server. Cache, etc.
-- look into garbage from openpgp signing
-- make leveldb memdb's iterator struct only 8 bytes, pointing to a recycled
object, and just nil out that pointer at EOF.
-- bring in the google glog package to third_party and use it in
places that want selective logging (e.g. pkg/index/receive.go)
-- (Mostly done) verify all ReceiveBlob calls and see which should be
blobserver.Receive instead, or ReceiveNoHash. git grep -E
"\.ReceiveBlob\(" And maybe ReceiveNoHash should go away and be
replaced with a "ReceiveString" method which combines the
blobref-from-string and ReceiveNoHash at once.
-- union storage target. sharder can be thought of a specialization
of union. sharder already unions, but has a hard-coded policy
of where to put new blobs. union could a library (used by sharder)
with a pluggable policy on that.
-- support for running cammount under camlistored. especially for OS X,
where the lifetime of the background daemon will be the same as the
user's login session.
-- website: remove the "Installation" heading for /cmd/*, since
they're misleading and people should use "go run make.go" in the
general case.
-- website: add godoc for /server/camlistored (also without a "go get"
line)
-- tests for all cmd/* stuff, perhaps as part of some integration
tests.
-- move most of camput into a library, not a package main.
-- server cron support: full syncs, camput file backups, integrity
checks.
-- status in top right of UI: sync, crons. (in-progress, un-acked
problems)
-- finish metadata compaction on the encryption blobserver.Storage wrapper.
-- get security review on encryption wrapper. (agl?)
-- peer-to-peer server and blobserver target to store encrypted blobs
on stranger's hardrives. server will be open source so groups of
friends/family can run their own for small circles, or some company
could run a huge instance. spray encrypted backup chunks across
friends' machines, and have central server(s) present challenges to
the replicas to have them verify what they have and how big, and
also occasionally say what the SHA-1("challenge" + blob-data) is.
-- sharing: make camget work with permanode sets too, not just
"directory" and "file" things.
-- sharing: when hitting e.g. http://myserver/share/sha1-xxxxx, if
a web browser and not a smart client (Accept header? User-Agent?)
then redirect or render a cutesy gallery or file browser instead,
still with machine-readable data for slurping.
-- rethink the directory schema so it can a) represent directories
with millions of files (without making a >1MB or >16MB schema blob),
probably forming a tree, similar to files. but rather than rolling checksum,
just split lexically when nodes get too big.
-- delete mostly-obsolete camsigd. see big TODO in camsigd.go.
-- we used to be able live-edit js/css files in server/camlistored/ui when
running under the App Engine dev_appserver.py. That's now broken with my
latest efforts to revive it. The place to start looking is:
server/camlistored/ui/fileembed_appengine.go
-- should a "share" claim be not a claim but its own permanode, so it
can be rescinded? right now you can't really unshare a "haveref"
claim. or rather, TODO: verify we support "delete" claims to
delete any claim, and verify the share system and indexer all
support it. I think the indexer might, but not the share system.
Also TODO: "camput delete" or "rescind" subcommand.
Also TODO: document share claims in doc/schema/ and on website.
-- make the -transitive flag for "camput share -transitive" be a tri-state:
unset, true, false, and unset should then mean default to true for "file"
and "directory" schema blobs, and "false" for other things.
-- index: static directory recursive sizes: search: ask to see biggest directories?
-- index: index dates in filenames ("yyyy-mm-dd-Foo-Trip", "yyyy-mm blah", etc).
-- get webdav server working again, for mounting on Windows. This worked before Go 1
but bitrot when we moved pkg/fs to use the rsc/fuse.
-- work on runsit more, so I can start using this more often. runsit should
be able to reload itself, and also watch for binaries changing and restart
when binaries change. (or symlinks to binaries)
-- BUG: osutil paths.go on OS X: should use Library everywhere instead of mix of
Library and ~/.camlistore?
OLD:
-- add CROS support? Access-Control-Allow-Origin: * + w/ OPTIONS
http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/
-- brackup integration, perhaps sans GPG? (requires Perl client?)
-- blobserver: clean up channel-closing consistency in blobserver interface
(most close, one doesn't. all should probably close)
Android:
[ ] Fix wake locks in UploadThread. need to hold CPU + WiFi whenever
something's enqueued at all and we're running. Move out of the Thread
that's uploading itself.
[ ] GPG signing of blobs (brad)
http://code.google.com/p/android-privacy-guard/
http://www.thialfihar.org/projects/apg/
(supports signing in code, but not an Intent?)
http://code.google.com/p/android-privacy-guard/wiki/UsingApgForDevelopment
... mailed the author.
Client libraries:
[X] Go
[X] JavaScript
[/] Python (Brett); but see https://github.com/tsileo/camlipy
[ ] Perl
[ ] Ruby
[ ] PHP

View File

@@ -0,0 +1,97 @@
/*
Copyright 2014 The Camlistore Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// The hello application serves as an example on how to make stand-alone
// server applications, interacting with a Camlistore server.
package main
import (
"flag"
"fmt"
"log"
"net/http"
"os"
"runtime"
"camlistore.org/pkg/app"
"camlistore.org/pkg/buildinfo"
"camlistore.org/pkg/webserver"
)
var (
flagVersion = flag.Bool("version", false, "show version")
)
// config is used to unmarshal the application configuration JSON
// that we get from Camlistore when we request it at $CAMLI_APP_CONFIG_URL.
type config struct {
Word string `json:"word,omitempty"` // Argument printed after "Hello " in the helloHandler response.
}
func appConfig() *config {
configURL := os.Getenv("CAMLI_APP_CONFIG_URL")
if configURL == "" {
log.Fatalf("Hello application needs a CAMLI_APP_CONFIG_URL env var")
}
cl, err := app.Client()
if err != nil {
log.Fatalf("could not get a client to fetch extra config: %v", err)
}
conf := &config{}
if err := cl.GetJSON(configURL, conf); err != nil {
log.Fatalf("could not get app config at %v: %v", configURL, err)
}
return conf
}
type helloHandler struct {
who string // who to say hello to.
}
func (h *helloHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("Content-Type", "text/plain; charset=utf-8")
rw.WriteHeader(200)
fmt.Fprintf(rw, "Hello %s\n", h.who)
}
func main() {
flag.Parse()
if *flagVersion {
fmt.Fprintf(os.Stderr, "hello version: %s\nGo version: %s (%s/%s)\n",
buildinfo.Version(), runtime.Version(), runtime.GOOS, runtime.GOARCH)
return
}
log.Printf("Starting hello version %s; Go %s (%s/%s)", buildinfo.Version(), runtime.Version(),
runtime.GOOS, runtime.GOARCH)
listenAddr, err := app.ListenAddress()
if err != nil {
log.Fatalf("Listen address: %v", err)
}
conf := appConfig()
ws := webserver.New()
ws.Handle("/", &helloHandler{who: conf.Word})
// TODO(mpl): handle status requests too. Camlistore will send an auth
// token in the extra config that should be used as the "password" for
// subsequent status requests.
if err := ws.Listen(listenAddr); err != nil {
log.Fatalf("Listen: %v", err)
}
ws.Serve()
}

View File

@@ -0,0 +1,32 @@
/*
Copyright 2014 The Camlistore Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
The publisher application serves and renders items published by Camlistore.
That is, items that are children, through a (direct or not) camliPath relation,
of a camliRoot node (a permanode with a camliRoot attribute set).
#fileembed pattern .+\.(js|css|html|png|svg)$
*/
package main
import (
"camlistore.org/pkg/fileembed"
)
// TODO(mpl): appengine case
var Files = &fileembed.Files{}

View File

@@ -0,0 +1,53 @@
<!doctype html>
<html>
{{if $header := call .Header}}
<head>
<title>{{$header.Title}}</title>
{{range $css := $header.CSSFiles}}
<link rel='stylesheet' type='text/css' href='{{$css}}'>
{{end}}
<script>
var camliViewIsOwner = {{$header.ViewerIsOwner}};
var camliPagePermanode = {{$header.Subject}};
var camliPageMeta =
{{$header.Meta}};
</script>
</head>
<body>
<h1>{{$header.Title}}</h1>
{{if $file := call .File}}
<div>File: {{$file.FileName}}, {{$file.Size}} bytes, type {{$file.MIMEType}}</div>
{{if $file.IsImage}}
<a href='{{$file.DownloadURL}}'><img src='{{$file.ThumbnailURL}}'></a>
{{end}}
<div id='{{$file.DomID}}' class='camlifile'>[<a href='{{$file.DownloadURL}}'>download</a>]</div>
{{if $nav := call $file.Nav}}
<div class='camlifile'>
{{if $prev := $nav.PrevPath}}[<a href='{{$prev}}'>prev</a>] {{end}}
{{if $up := $nav.ParentPath}}[<a href='{{$up}}'>up</a>] {{end}}
{{if $next := $nav.NextPath}}[<a href='{{$next}}'>next</a>] {{end}}
</div>
{{end}}
{{else}}
{{if $membersData := call .Members}}
<div><a href='{{$membersData.SubjectPath}}/=z/{{html $membersData.ZipName | urlquery}}'>{{html $membersData.ZipName}}</a></div>
<!-- TODO(mpl): something's messed up with the hidden edit title position, it should appear under the image. -->
<ul id='members'>
{{range $member := $membersData.Members}}
<li id='{{call $membersData.DomID $member}}'>
<a href='{{call $membersData.Path $member}}'>
{{$fileInfo := call $membersData.FileInfo $member}}
<img src='{{if $fileInfo}}{{$fileInfo.FileThumbnailURL}}{{end}}'>
<span>{{call $membersData.Title $member}}</span></a>
{{call $membersData.Description $member}}
<div id='{{if $fileInfo}}{{$fileInfo.FileDomID}}{{end}}' class='camlifile'>
<a href='{{if $fileInfo}}{{$fileInfo.FilePath}}{{end}}'>file</a>
</div>
</li>
{{end}}
</ul>
{{end}}
{{end}}
{{end}}
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,112 @@
/*
Copyright 2012 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* Something arbitrary for testing. */
body {
font: 13px/1.3 normal Verdana, Geneva, sans-serif;
background: #000;
color: #aaa;
margin: 0;
padding: 30px;
}
a {
color: #aaa;
}
a:hover {
color: #bbb;
}
h1 {
border: 3px dashed #aaa;
padding: 1em;
}
ul {
list-style: none;
background: #262626;
margin: 0;
padding: 20px;
border-radius: 10px;
}
li {
display: inline-block;
vertical-align: top;
padding: 1em;
margin: 1em;
text-align: right;
}
li:hover {
background: #000;
border-radius: 10px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
li:hover img {
xmax-height: none;
xmax-width: none;
}
li a {
font-weight: bold;
}
li img {
border: 0;
border-radius: 6px;
display: block;
max-height: 200px;
max-width: 200px;
margin-bottom: 1em;
}
li .camlifile {
display: none;
}
li a span {
padding: 2px;
border: 1px solid transparent;
}
li input {
text-align: right;
}
a.title-edit,
a.title-edit:hover {
font-size: 70%;
color: #f00;
margin-right: .5em;
font-weight: normal;
}
a.hidden {
display: none;
}
a.visible {
display: inline;
}
input.hidden {
display: none;
}
input.visible {
display: inline-block;
}
span.hidden {
display: none;
}
span.visible {
display: inline-block;
}

View File

@@ -0,0 +1,234 @@
/*
Copyright 2014 The Camlistore Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
camliClient "camlistore.org/pkg/client"
"camlistore.org/pkg/httputil"
"camlistore.org/pkg/index"
"camlistore.org/pkg/index/indextest"
"camlistore.org/pkg/search"
)
type publishURLTest struct {
path string // input
subject, subres string // expected
}
var publishURLTests []publishURLTest
func setupContent(rootName string) *indextest.IndexDeps {
idx := index.NewMemoryIndex()
idxd := indextest.NewIndexDeps(idx)
picNode := idxd.NewPlannedPermanode("picpn-1234") // sha1-f5e90fcc50a79caa8b22a4aa63ba92e436cab9ec
galRef := idxd.NewPlannedPermanode("gal-1234") // sha1-2bdf2053922c3dfa70b01a4827168fce1c1df691
rootRef := idxd.NewPlannedPermanode("root-abcd") // sha1-dbb3e5f28c7e01536d43ce194f3dd7b921b8460d
camp0 := idxd.NewPlannedPermanode("picpn-9876543210") // sha1-2d473e07ca760231dd82edeef4019d5b7d0ccb42
camp1 := idxd.NewPlannedPermanode("picpn-9876543211") // sha1-961b700536d5151fc1f3920955cc92767572a064
camp0f, _ := idxd.UploadFile("picfile-f00ff00f00a5.jpg", "picfile-f00ff00f00a5", time.Time{}) // sha1-01dbcb193fc789033fb2d08ed22abe7105b48640
camp1f, _ := idxd.UploadFile("picfile-f00ff00f00b6.jpg", "picfile-f00ff00f00b6", time.Time{}) // sha1-1213ec17a42cc51bdeb95ff91ac1b5fc5157740f
idxd.SetAttribute(rootRef, "camliRoot", rootName)
idxd.SetAttribute(rootRef, "camliPath:singlepic", picNode.String())
idxd.SetAttribute(picNode, "title", "picnode without a pic?")
idxd.SetAttribute(rootRef, "camliPath:camping", galRef.String())
idxd.AddAttribute(galRef, "camliMember", camp0.String())
idxd.AddAttribute(galRef, "camliMember", camp1.String())
idxd.SetAttribute(camp0, "camliContent", camp0f.String())
idxd.SetAttribute(camp1, "camliContent", camp1f.String())
publishURLTests = []publishURLTest{
// URL to a single picture permanode (returning its HTML wrapper page)
{
path: "/pics/singlepic",
subject: picNode.String(),
},
// URL to a gallery permanode (returning its HTML wrapper page)
{
path: "/pics/camping",
subject: galRef.String(),
},
// URL to a picture permanode within a gallery (following one hop, returning HTML)
{
path: "/pics/camping/-/h2d473e07ca",
subject: camp0.String(),
},
// URL to a gallery -> picture permanode -> its file
// (following two hops, returning HTML)
{
path: "/pics/camping/-/h2d473e07ca/h01dbcb193f",
subject: camp0f.String(),
},
// URL to a gallery -> picture permanode -> its file
// (following two hops, returning the file download)
{
path: "/pics/camping/-/h2d473e07ca/h01dbcb193f/=f/marshmallow.jpg",
subject: camp0f.String(),
subres: "/=f/marshmallow.jpg",
},
// URL to a gallery -> picture permanode -> its file
// (following two hops, returning the file, scaled as an image)
{
path: "/pics/camping/-/h961b700536/h1213ec17a4/=i/marshmallow.jpg?mw=200&mh=200",
subject: camp1f.String(),
subres: "/=i/marshmallow.jpg",
},
// Path to a static file in the root.
// TODO: ditch these and use content-addressable javascript + css, having
// the server digest them on start, or rather part of fileembed. This is
// a short-term hack to unblock Lindsey.
{
path: "/pics/=s/pics.js",
subject: "",
subres: "/=s/pics.js",
},
}
return idxd
}
type fakeClient struct {
*camliClient.Client // for blob.Fetcher
sh *search.Handler
}
func (fc *fakeClient) Search(req *search.SearchQuery) (*search.SearchResult, error) {
return fc.sh.Query(req)
}
func (fc *fakeClient) Describe(req *search.DescribeRequest) (*search.DescribeResponse, error) {
return fc.sh.Describe(req)
}
func (fc *fakeClient) GetJSON(url string, data interface{}) error {
// no need to implement
return nil
}
func (fc *fakeClient) Post(url string, bodyType string, body io.Reader) error {
// no need to implement
return nil
}
func TestPublishURLs(t *testing.T) {
rootName := "foo"
idxd := setupContent(rootName)
sh := search.NewHandler(idxd.Index, idxd.SignerBlobRef)
corpus, err := idxd.Index.KeepInMemory()
if err != nil {
t.Fatalf("error slurping index to memory: %v", err)
}
sh.SetCorpus(corpus)
cl := camliClient.New("http://whatever.fake")
fcl := &fakeClient{cl, sh}
ph := &publishHandler{
rootName: rootName,
cl: fcl,
}
if err := ph.initRootNode(); err != nil {
t.Fatalf("initRootNode: %v", err)
}
for ti, tt := range publishURLTests {
rw := httptest.NewRecorder()
if !strings.HasPrefix(tt.path, "/pics/") {
panic("expected /pics/ prefix on " + tt.path)
}
req, _ := http.NewRequest("GET", "http://foo.com"+tt.path, nil)
pfxh := &httputil.PrefixHandler{
Prefix: "/pics/",
Handler: http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
pr, err := ph.NewRequest(rw, req)
if err != nil {
t.Fatalf("test #%d, NewRequest: %v", ti, err)
}
err = pr.findSubject()
if tt.subject != "" {
if err != nil {
t.Errorf("test #%d, findSubject: %v", ti, err)
return
}
if pr.subject.String() != tt.subject {
t.Errorf("test #%d, got subject %q, want %q", ti, pr.subject, tt.subject)
}
}
if pr.subres != tt.subres {
t.Errorf("test #%d, got subres %q, want %q", ti, pr.subres, tt.subres)
}
}),
}
pfxh.ServeHTTP(rw, req)
}
}
func TestPublishMembers(t *testing.T) {
rootName := "foo"
idxd := setupContent(rootName)
sh := search.NewHandler(idxd.Index, idxd.SignerBlobRef)
corpus, err := idxd.Index.KeepInMemory()
if err != nil {
t.Fatalf("error slurping index to memory: %v", err)
}
sh.SetCorpus(corpus)
cl := camliClient.New("http://whatever.fake")
fcl := &fakeClient{cl, sh}
ph := &publishHandler{
rootName: rootName,
cl: fcl,
}
rw := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "http://foo.com/pics", nil)
pfxh := &httputil.PrefixHandler{
Prefix: "/pics/",
Handler: http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
pr, err := ph.NewRequest(rw, req)
if err != nil {
t.Fatalf("NewRequest: %v", err)
}
res, err := pr.ph.deepDescribe(pr.subject)
if err != nil {
t.Fatalf("deepDescribe: %v", err)
}
members, err := pr.subjectMembers(res.Meta)
if len(members.Members) != 2 {
t.Errorf("Expected two members in publish root (one camlipath, one camlimember), got %d", len(members.Members))
}
}),
}
pfxh.ServeHTTP(rw, req)
}

View File

@@ -0,0 +1,308 @@
/*
Copyright 2013 The Camlistore Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"archive/zip"
"crypto/sha1"
"fmt"
"io"
"log"
"mime"
"net/http"
"path"
"sort"
"camlistore.org/pkg/blob"
"camlistore.org/pkg/httputil"
"camlistore.org/pkg/schema"
"camlistore.org/pkg/search"
"camlistore.org/pkg/types/camtypes"
)
type zipHandler struct {
fetcher blob.Fetcher
cl client // Used for search and describe requests.
// root is the "parent" permanode of everything to zip.
// Either a directory permanode, or a permanode with members.
root blob.Ref
// Optional name to use in the response header
filename string
}
// blobFile contains all the information we need about
// a file blob to add the corresponding file to a zip.
type blobFile struct {
blobRef blob.Ref
// path is the full path of the file from the root of the zip.
// slashes are always forward slashes, per the zip spec.
path string
}
type sortedFiles []*blobFile
func (s sortedFiles) Less(i, j int) bool { return s[i].path < s[j].path }
func (s sortedFiles) Len() int { return len(s) }
func (s sortedFiles) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (zh *zipHandler) describeMembers(br blob.Ref) (*search.DescribeResponse, error) {
res, err := zh.cl.Query(&search.SearchQuery{
Constraint: &search.Constraint{
BlobRefPrefix: br.String(),
CamliType: "permanode",
},
Describe: &search.DescribeRequest{
Depth: 1,
Rules: []*search.DescribeRule{
{
Attrs: []string{"camliContent", "camliContentImage", "camliMember"},
},
},
},
Limit: -1,
})
if err != nil {
return nil, fmt.Errorf("Could not describe %v: %v", br, err)
}
if res == nil || res.Describe == nil {
return nil, fmt.Errorf("no describe result for %v", br)
}
return res.Describe, nil
}
// blobList returns the list of file blobs "under" dirBlob.
// It traverses permanode directories and permanode with members (collections).
func (zh *zipHandler) blobList(dirPath string, dirBlob blob.Ref) ([]*blobFile, error) {
// dr := zh.search.NewDescribeRequest()
// dr.Describe(dirBlob, 3)
// res, err := dr.Result()
// if err != nil {
// return nil, fmt.Errorf("Could not describe %v: %v", dirBlob, err)
// }
res, err := zh.describeMembers(dirBlob)
if err != nil {
return nil, err
}
described := res.Meta[dirBlob.String()]
members := described.Members()
dirBlobPath, _, isDir := described.PermanodeDir()
if len(members) == 0 && !isDir {
return nil, nil
}
var list []*blobFile
if isDir {
dirRoot := dirBlobPath[1]
children, err := zh.blobsFromDir("/", dirRoot)
if err != nil {
return nil, fmt.Errorf("Could not get list of blobs from %v: %v", dirRoot, err)
}
list = append(list, children...)
return list, nil
}
for _, member := range members {
if fileBlobPath, fileInfo, ok := getFileInfo(member.BlobRef, res.Meta); ok {
// file
list = append(list,
&blobFile{fileBlobPath[1], path.Join(dirPath, fileInfo.FileName)})
continue
}
if dirBlobPath, dirInfo, ok := getDirInfo(member.BlobRef, res.Meta); ok {
// directory
newZipRoot := dirBlobPath[1]
children, err := zh.blobsFromDir(
path.Join(dirPath, dirInfo.FileName), newZipRoot)
if err != nil {
return nil, fmt.Errorf("Could not get list of blobs from %v: %v", newZipRoot, err)
}
list = append(list, children...)
// TODO(mpl): we assume a directory permanode does not also have members.
// I know there is nothing preventing it, but does it make any sense?
continue
}
// it might have members, so recurse
// If it does have members, we must consider it as a pseudo dir,
// so we can build a fullpath for each of its members.
// As a dir name, we're using its title if it has one, its (shortened)
// blobref otherwise.
pseudoDirName := member.Title()
if pseudoDirName == "" {
pseudoDirName = member.BlobRef.DigestPrefix(10)
}
fullpath := path.Join(dirPath, pseudoDirName)
moreMembers, err := zh.blobList(fullpath, member.BlobRef)
if err != nil {
return nil, fmt.Errorf("Could not get list of blobs from %v: %v", member.BlobRef, err)
}
list = append(list, moreMembers...)
}
return list, nil
}
// blobsFromDir returns the list of file blobs in directory dirBlob.
// It only traverses permanode directories.
func (zh *zipHandler) blobsFromDir(dirPath string, dirBlob blob.Ref) ([]*blobFile, error) {
var list []*blobFile
dr, err := schema.NewDirReader(zh.fetcher, dirBlob)
if err != nil {
return nil, fmt.Errorf("Could not read dir blob %v: %v", dirBlob, err)
}
ent, err := dr.Readdir(-1)
if err != nil {
return nil, fmt.Errorf("Could not read dir entries: %v", err)
}
for _, v := range ent {
fullpath := path.Join(dirPath, v.FileName())
switch v.CamliType() {
case "file":
list = append(list, &blobFile{v.BlobRef(), fullpath})
case "directory":
children, err := zh.blobsFromDir(fullpath, v.BlobRef())
if err != nil {
return nil, fmt.Errorf("Could not get list of blobs from %v: %v", v.BlobRef(), err)
}
list = append(list, children...)
}
}
return list, nil
}
// renameDuplicates goes through bf to check for duplicate filepaths.
// It renames duplicate filepaths and returns a new slice, sorted by
// file path.
func renameDuplicates(bf []*blobFile) sortedFiles {
noDup := make(map[string]blob.Ref)
// use a map to detect duplicates and rename them
for _, file := range bf {
if _, ok := noDup[file.path]; ok {
// path already exists, so rename
suffix := 0
var newname string
for {
suffix++
ext := path.Ext(file.path)
newname = fmt.Sprintf("%s(%d)%s",
file.path[:len(file.path)-len(ext)], suffix, ext)
if _, ok := noDup[newname]; !ok {
break
}
}
noDup[newname] = file.blobRef
} else {
noDup[file.path] = file.blobRef
}
}
// reinsert in a slice and sort it
var sorted sortedFiles
for p, b := range noDup {
sorted = append(sorted, &blobFile{path: p, blobRef: b})
}
sort.Sort(sorted)
return sorted
}
// ServeHTTP streams a zip archive of all the files "under"
// zh.root. That is, all the files pointed by file permanodes,
// which are directly members of zh.root or recursively down
// directory permanodes and permanodes members.
// To build the fullpath of a file in a collection, it uses
// the collection title if present, its blobRef otherwise, as
// a directory name.
func (zh *zipHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// TODO: use http.ServeContent, so Range requests work and downloads can be resumed.
// Will require calculating the zip length once first (ideally as cheaply as possible,
// with dummy counting writer and dummy all-zero-byte-files of a fixed size),
// and then making a dummy ReadSeeker for ServeContent that can seek to the end,
// and then seek back to the beginning, but then seeks forward make it remember
// to skip that many bytes from the archive/zip writer when answering Reads.
if !httputil.IsGet(req) {
http.Error(rw, "Invalid method", http.StatusMethodNotAllowed)
return
}
bf, err := zh.blobList("/", zh.root)
if err != nil {
log.Printf("Could not serve zip for %v: %v", zh.root, err)
http.Error(rw, "Server error", http.StatusInternalServerError)
return
}
blobFiles := renameDuplicates(bf)
// TODO(mpl): streaming directly won't work on appengine if the size goes
// over 32 MB. Deal with that.
h := rw.Header()
h.Set("Content-Type", "application/zip")
filename := zh.filename
if filename == "" {
filename = "download.zip"
}
h.Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename}))
zw := zip.NewWriter(rw)
etag := sha1.New()
for _, file := range blobFiles {
etag.Write([]byte(file.blobRef.String()))
}
h.Set("Etag", fmt.Sprintf(`"%x"`, etag.Sum(nil)))
for _, file := range blobFiles {
fr, err := schema.NewFileReader(zh.fetcher, file.blobRef)
if err != nil {
log.Printf("Can not add %v in zip, not a file: %v", file.blobRef, err)
http.Error(rw, "Server error", http.StatusInternalServerError)
return
}
f, err := zw.CreateHeader(
&zip.FileHeader{
Name: file.path,
Method: zip.Store,
})
if err != nil {
log.Printf("Could not create %q in zip: %v", file.path, err)
http.Error(rw, "Server error", http.StatusInternalServerError)
return
}
_, err = io.Copy(f, fr)
fr.Close()
if err != nil {
log.Printf("Could not zip %q: %v", file.path, err)
return
}
}
err = zw.Close()
if err != nil {
log.Printf("Could not close zipwriter: %v", err)
return
}
}
// TODO(mpl): refactor with getFileInfo
func getDirInfo(item blob.Ref, peers map[string]*search.DescribedBlob) (path []blob.Ref, di *camtypes.FileInfo, ok bool) {
described := peers[item.String()]
if described == nil ||
described.Permanode == nil ||
described.Permanode.Attr == nil {
return
}
contentRef := described.Permanode.Attr.Get("camliContent")
if contentRef == "" {
return
}
if cdes := peers[contentRef]; cdes != nil && cdes.Dir != nil {
return []blob.Ref{described.BlobRef, cdes.BlobRef}, cdes.Dir, true
}
return
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>

View File

@@ -0,0 +1,8 @@
build
gen
bin
local.properties
test/local.properties
test/build
test/gen
test/bin

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>camlistore</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,301 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.codeComplete.argumentPrefixes=
org.eclipse.jdt.core.codeComplete.argumentSuffixes=
org.eclipse.jdt.core.codeComplete.fieldPrefixes=
org.eclipse.jdt.core.codeComplete.fieldSuffixes=
org.eclipse.jdt.core.codeComplete.localPrefixes=
org.eclipse.jdt.core.codeComplete.localSuffixes=
org.eclipse.jdt.core.codeComplete.staticFieldPrefixes=
org.eclipse.jdt.core.codeComplete.staticFieldSuffixes=
org.eclipse.jdt.core.codeComplete.staticFinalFieldPrefixes=
org.eclipse.jdt.core.codeComplete.staticFinalFieldSuffixes=
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.6
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.source=1.6
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16
org.eclipse.jdt.core.formatter.alignment_for_assignment=0
org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16
org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80
org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0
org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0
org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80
org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16
org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16
org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
org.eclipse.jdt.core.formatter.blank_lines_after_package=1
org.eclipse.jdt.core.formatter.blank_lines_before_field=0
org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0
org.eclipse.jdt.core.formatter.blank_lines_before_imports=1
org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1
org.eclipse.jdt.core.formatter.blank_lines_before_method=1
org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
org.eclipse.jdt.core.formatter.blank_lines_before_package=0
org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1
org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false
org.eclipse.jdt.core.formatter.comment.format_block_comments=true
org.eclipse.jdt.core.formatter.comment.format_header=false
org.eclipse.jdt.core.formatter.comment.format_html=true
org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
org.eclipse.jdt.core.formatter.comment.format_line_comments=false
org.eclipse.jdt.core.formatter.comment.format_source_code=true
org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true
org.eclipse.jdt.core.formatter.comment.indent_root_tags=true
org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=insert
org.eclipse.jdt.core.formatter.comment.line_length=500
org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true
org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true
org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false
org.eclipse.jdt.core.formatter.compact_else_if=true
org.eclipse.jdt.core.formatter.continuation_indentation=2
org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off
org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on
org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true
org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
org.eclipse.jdt.core.formatter.indent_empty_lines=false
org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false
org.eclipse.jdt.core.formatter.indentation.size=4
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert
org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert
org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert
org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert
org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert
org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert
org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert
org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert
org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert
org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert
org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
org.eclipse.jdt.core.formatter.join_lines_in_comments=true
org.eclipse.jdt.core.formatter.join_wrapped_lines=false
org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false
org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false
org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
org.eclipse.jdt.core.formatter.lineSplit=200
org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1
org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true
org.eclipse.jdt.core.formatter.tabulation.char=space
org.eclipse.jdt.core.formatter.tabulation.size=4
org.eclipse.jdt.core.formatter.use_on_off_tags=false
org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true
org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true

View File

@@ -0,0 +1,60 @@
eclipse.preferences.version=1
editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
formatter_profile=_Camlistore Policy
formatter_settings_version=12
org.eclipse.jdt.ui.exception.name=e
org.eclipse.jdt.ui.gettersetter.use.is=true
org.eclipse.jdt.ui.keywordthis=false
org.eclipse.jdt.ui.overrideannotation=true
sp_cleanup.add_default_serial_version_id=true
sp_cleanup.add_generated_serial_version_id=false
sp_cleanup.add_missing_annotations=true
sp_cleanup.add_missing_deprecated_annotations=true
sp_cleanup.add_missing_methods=false
sp_cleanup.add_missing_nls_tags=false
sp_cleanup.add_missing_override_annotations=true
sp_cleanup.add_missing_override_annotations_interface_methods=true
sp_cleanup.add_serial_version_id=false
sp_cleanup.always_use_blocks=true
sp_cleanup.always_use_parentheses_in_expressions=false
sp_cleanup.always_use_this_for_non_static_field_access=false
sp_cleanup.always_use_this_for_non_static_method_access=false
sp_cleanup.convert_to_enhanced_for_loop=false
sp_cleanup.correct_indentation=true
sp_cleanup.format_source_code=true
sp_cleanup.format_source_code_changes_only=false
sp_cleanup.make_local_variable_final=false
sp_cleanup.make_parameters_final=false
sp_cleanup.make_private_fields_final=true
sp_cleanup.make_type_abstract_if_missing_method=false
sp_cleanup.make_variable_declarations_final=true
sp_cleanup.never_use_blocks=false
sp_cleanup.never_use_parentheses_in_expressions=true
sp_cleanup.on_save_use_additional_actions=true
sp_cleanup.organize_imports=true
sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
sp_cleanup.remove_private_constructors=true
sp_cleanup.remove_trailing_whitespaces=true
sp_cleanup.remove_trailing_whitespaces_all=true
sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
sp_cleanup.remove_unnecessary_casts=true
sp_cleanup.remove_unnecessary_nls_tags=false
sp_cleanup.remove_unused_imports=true
sp_cleanup.remove_unused_local_variables=false
sp_cleanup.remove_unused_private_fields=true
sp_cleanup.remove_unused_private_members=false
sp_cleanup.remove_unused_private_methods=true
sp_cleanup.remove_unused_private_types=true
sp_cleanup.sort_members=false
sp_cleanup.sort_members_all=false
sp_cleanup.use_blocks=false
sp_cleanup.use_blocks_only_for_return_and_throw=false
sp_cleanup.use_parentheses_in_expressions=false
sp_cleanup.use_this_for_non_static_field_access=false
sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
sp_cleanup.use_this_for_non_static_method_access=false
sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.camlistore"
android:versionCode="2"
android:versionName="0.6.1">
<!-- Now using Gingerbread and up.... -->
<uses-sdk android:minSdkVersion="10" android:targetSdkVersion="17" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.BATTERY_STATS" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application android:icon="@drawable/icon" android:label="@string/app_name"
android:name=".UploadApplication" android:allowBackup="true">
<service android:name=".UploadService"
android:exported="false"
android:label="Camlistore Upload Service" />
<activity android:name=".CamliActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="*/*" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<data android:mimeType="*/*" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name=".BrowseActivity">
</activity>
<activity android:name=".SettingsActivity">
</activity>
<receiver android:name=".OnBootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name=".OnAlarmReceiver">
</receiver>
<receiver android:name=".WifiPowerReceiver"
android:enabled="true"
android:priority="0">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.ACTION_POWER_CONNECTED" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.ACTION_POWER_DISCONNECTED" />
</intent-filter>
</receiver>
</application>
</manifest>

View File

@@ -0,0 +1,17 @@
all:
./check-environment.pl
ant debug
# Dummy target to make build.pl happy
install:
./check-environment.pl
ant debug
env:
docker build -t camlistore/android devenv
dockerdebug:
docker run -v $(GOPATH)/src/camlistore.org:/src/camlistore.org camlistore/android /src/camlistore.org/clients/android/build-in-docker.pl debug
dockerrelease:
docker run -i -t -v $(GOPATH)/src/camlistore.org:/src/camlistore.org -v $(HOME)/keys/android-camlistore:/keys camlistore/android /src/camlistore.org/clients/android/build-in-docker.pl release

View File

@@ -0,0 +1,64 @@
#!/usr/bin/perl
use strict;
use File::Path qw(make_path);
die "This script is meant to be run within the camlistore/android Docker contain. Run 'make env' to build it.\n"
unless $ENV{IN_DOCKER};
my $mode = shift || "debug";
my $ANDROID = "/src/camlistore.org/clients/android";
my $ASSETS = "$ANDROID/assets";
my $GENDIR = "$ANDROID/gen/org/camlistore";
umask 0;
make_path($GENDIR, { mode => 0755 }) unless -d $GENDIR;
$ENV{GOROOT} = "/usr/local/go";
$ENV{GOBIN} = $GENDIR;
$ENV{GOPATH} = "/";
$ENV{GOARCH} = "arm";
print "Building ARM camlistore.org/cmd/camput\n";
system("/usr/local/go/bin/go", "install", "camlistore.org/cmd/camput")
and die "Failed to build camput";
system("cp", "-p", "$GENDIR/linux_arm/camput", "$ASSETS/camput.arm")
and die "cp failure";
# TODO: build an x86 version too? if/when those Android devices matter.
{
open(my $vfh, ">$ASSETS/camput-version.txt") or die "open camput-version error: $!";
# TODO(bradfitz): make these values automatic, and don't make the
# "Version" menu say "camput version" when it runs. Also maybe
# keep a history of these somewhere more convenient.
print $vfh "app 0.6.1 camput ccacf764 go 70499e5fbe5b";
}
chdir $ASSETS or die "can't cd to assets dir";
my $digest = `openssl sha1 camput.arm`;
chomp $digest;
print "ARM camput is $digest\n";
die "No digest" unless $digest;
write_file("$GENDIR/ChildProcessConfig.java", "package org.camlistore; public final class ChildProcessConfig { // $digest\n}");
print "Running ant $mode\n";
chdir $ANDROID or die "can't cd to android dir";
exec "ant",
"-Dsdk.dir=/usr/local/android-sdk-linux",
"-Dkey.store=/keys/android-camlistore.keystore",
"-Dkey.alias=camkey",
$mode;
sub write_file {
my ($file, $contents) = @_;
if (open(my $fh, $file)) {
my $cur = do { local $/; <$fh> };
return if $cur eq $contents;
}
open(my $fh, ">$file") or die "Failed to open $file: $!";
print $fh $contents;
close($fh) or die "Close: $!";
print "Wrote $file\n";
}

View File

@@ -0,0 +1,2 @@
out.dir=build
gen.dir=build/gen

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="CamliActivity" default="help">
<!-- The local.properties file is created and updated by the 'android' tool.
It contains the path to the SDK. It should *NOT* be checked in in Version
Control Systems. -->
<property file="local.properties" />
<!-- The build.properties file can be created by you and is never touched
by the 'android' tool. This is the place to change some of the default property values
used by the Ant rules.
Here are some properties you may want to change/update:
application.package
the name of your application package as defined in the manifest. Used by the
'uninstall' rule.
source.dir
the name of the source directory. Default is 'src'.
out.dir
the name of the output directory. Default is 'bin'.
Properties related to the SDK location or the project target should be updated
using the 'android' tool with the 'update' action.
This file is an integral part of the build system for your application and
should be checked in in Version Control Systems.
-->
<property file="build.properties" />
<!-- The default.properties file is created and updated by the 'android' tool, as well
as ADT.
This file is an integral part of the build system for your application and
should be checked in in Version Control Systems. -->
<property file="default.properties" />
<!--
Import per project custom build rules if present at the root of the project.
This is the place to put custom intermediary targets such as:
-pre-build
-pre-compile
-post-compile (This is typically used for code obfuscation.
Compiled code location: ${out.classes.absolute.dir}
If this is not done in place, override ${out.dex.input.absolute.dir})
-post-package
-post-build
-pre-clean
-->
<import file="custom_rules.xml" optional="true" />
<!-- Import the actual build file.
To customize existing targets, there are two options:
- Customize only one target:
- copy/paste the target into this file, *before* the
<import> task.
- customize it to your needs.
- Customize the whole content of build.xml
- copy/paste the content of the rules files (minus the top node)
into this file, replacing the <import> task.
- customize to your needs.
***********************
****** IMPORTANT ******
***********************
In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
in order to avoid having your file be overridden by tools such as "android update project"
-->
<!-- version-tag: 1 -->
<import file="${sdk.dir}/tools/ant/build.xml" />
</project>

View File

@@ -0,0 +1,14 @@
#!/usr/bin/perl
use strict;
use FindBin qw($Bin);
my $props = "$Bin/local.properties";
unless (-e $props) {
die "\n".
"**************************************************************\n".
"Can't build the Camlistore Android client; SDK not configured.\n".
"You need to create your $props file.\n".
"See local.properties.TEMPLATE for instructions.\n".
"**************************************************************\n\n";
}

View File

@@ -0,0 +1,12 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system use,
# "build.properties", and override values to adapt the script to your
# project structure.
# Project target.
target=android-17

View File

@@ -0,0 +1,38 @@
# Build environment in which to build the Camlistore Android app.
#
# This extends the Dockerfile from https://index.docker.io/u/wasabeef/android/
FROM wasabeef/android
MAINTAINER bradfitz <brad@danga.com>
# Found these from: android list sdk -u -e
RUN android list sdk -u -e | grep build-tools- | perl -npe 's/.+"(.+)"/$1/' > /tmp/build-tools-version
RUN perl -e 'die "No Android build tools version found." unless -s "/tmp/build-tools-version"'
RUN echo y | android update sdk -u -t $(cat /tmp/build-tools-version)
RUN echo y | android update sdk -u -t android-17
# Don't need mercurial yet, since we're just using the archive URL to fetch Go.
# But it's possible we may want to switch to using hg, in which case:
# RUN yum -y install mercurial
# Update the GOVERS to depend on a new version of Go.
#
# The 073fc578434b version is Go 1.3.1 (2014-02-21),
# to satisfy the dependency for Go 1.3 in the Docker build of
# camput.
ENV GOVERS 073fc578434b
RUN cd /usr/local && curl -O http://go.googlecode.com/archive/$GOVERS.zip
RUN cd /usr/local && unzip -q $GOVERS.zip
RUN cd /usr/local && mv go-$GOVERS go
RUN chmod 0755 /usr/local/go/src/make.bash
RUN echo $GOVERS > /usr/local/go/VERSION
RUN GOROOT=/usr/local/go GOARCH=arm bash -c "cd /usr/local/go/src && ./make.bash"
ENV ANDROID_HOME /usr/local/android-sdk-linux
ENV ANT_HOME /usr/local/apache-ant-1.9.2
ENV PATH $PATH:$ANDROID_HOME/tools
ENV PATH $PATH:$ANDROID_HOME/platform-tools
ENV PATH $PATH:$ANT_HOME/bin
ENV IN_DOCKER 1

View File

@@ -0,0 +1,3 @@
# Copy this file to one named "local.properties" and update this path to
# wherever your Android SDK is located:
sdk.dir=/home/bradfitz/sdk/android

View File

@@ -0,0 +1,14 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-17

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,77 @@
<?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"
android:padding="10sp" >
<TextView
android:id="@+id/textStatus"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Camlistore Uploader" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="horizontal"
android:padding="5px" >
<Button
android:id="@+id/buttonToggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="15sp"
android:text="@+string/pause_resume" />
</LinearLayout>
<TextView
android:id="@+id/textBlobsRemain"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@+string/blobs_remaining" />
<TextView
android:id="@+id/textUploadStatus"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@+string/status_detail" />
<TextView
android:id="@+id/textByteStatus"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@+string/bytes_uploaded" />
<ProgressBar
android:id="@+id/progressByteStatus"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="fill_parent"
android:layout_height="20sp"
android:indeterminate="false"
android:indeterminateOnly="false"
android:minHeight="20sp" />
<TextView
android:id="@+id/textFileStatus"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@+string/files_uploaded" />
<ProgressBar
android:id="@+id/progressFileStatus"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="fill_parent"
android:layout_height="20sp"
android:indeterminate="false"
android:indeterminateOnly="false"
android:minHeight="20sp" />
<TextView
android:id="@+id/textStats"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="" />
</LinearLayout>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Camlistore Uploader</string>
<string name="settings_confirmation_dialog_title">Use these settings?</string>
<string name="settings_host_title">Camlistore server</string>
<string name="settings_host_summary">e.g. https://foo.example.com or &quot;example.com:3179&quot;</string>
<string name="settings_qr_title">QR</string>
<string name="settings_qr_summary">Scan QR code from /ui/mobile.html</string>
<string name="settings_trusted_cert_title">Self-signed cert fingerprint</string>
<string name="settings_trusted_cert_summary">The fingerprint of your self-signed certificate. Not needed for commercial certs.</string>
<string name="settings_username_title">Username</string>
<string name="settings_password_title">Password</string>
<string name="settings_max_cache_size_title">Maximum cache size</string>
<string name="settings_max_cache_size_summary">%d MB</string>
<string name="cancel">Cancel</string>
<string name="ok">OK</string>
<string name="pause_resume">Pause / Resume</string>
<string name="pause">Pause</string>
<string name="resume">Resume</string>
<string name="stop">Stop</string>
<string name="stop_die">Force-kill all</string>
<string name="version">Version</string>
<string name="uploading">Uploading&#8230;</string>
<string name="digesting">Digesting&#8230;.</string>
<string name="settings_auto">Auto-Upload</string>
<string name="settings_auto_summary">Upload SD card files as created</string>
<string name="settings_dev_ip">Development IP address</string>
<string name="files_remaining">Files Remaining: &#8212;</string>
<string name="status_detail">[status detail]</string>
<string name="files_uploaded">Files Uploaded</string>
<string name="bytes_uploaded">Bytes Uploaded</string>
<string name="settings">Settings</string>
<string name="upload_all">Upload All</string>
<string name="browse">Browse</string>
<string name="results">Results</string>
<string name="settings_auto_required_ssid">Required SSID</string>
</resources>

View File

@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:key="first_preferencescreen" >
<org.camlistore.QRPreference
android:key="camli.qr"
android:summary="@string/settings_qr_summary"
android:title="@string/settings_qr_title"/>
<EditTextPreference
android:key="camli.host"
android:persistent="true"
android:summary="@string/settings_host_summary"
android:title="@string/settings_host_title" />
<EditTextPreference
android:key="camli.username"
android:persistent="true"
android:title="@string/settings_username_title" />
<EditTextPreference
android:inputType="textPassword"
android:key="camli.password"
android:persistent="true"
android:title="@string/settings_password_title" />
<CheckBoxPreference
android:key="camli.auto"
android:persistent="true"
android:summary="@string/settings_auto_summary"
android:title="@string/settings_auto" />
<PreferenceScreen
android:key="camli.auto.opts"
android:title="Auto-upload settings" >
<CheckBoxPreference
android:defaultValue="true"
android:key="camli.auto.photos"
android:persistent="true"
android:title="Photos (DCIM/Camera/)" />
<CheckBoxPreference
android:defaultValue="true"
android:key="camli.auto.mytracks"
android:persistent="true"
android:title="MyTracks exports" />
<CheckBoxPreference
android:defaultValue="false"
android:key="camli.auto.require_wifi"
android:persistent="true"
android:summary="Wait for Wifi to auto-upload"
android:title="Require Wifi" />
<EditTextPreference
android:key="camli.auto.required_wifi_ssid"
android:persistent="true"
android:singleLine="true"
android:summary="Restrict auto-upload to this SSID"
android:title="@string/settings_auto_required_ssid" />
<CheckBoxPreference
android:defaultValue="false"
android:key="camli.auto.require_power"
android:persistent="true"
android:summary="Wait until charging to auto-upload"
android:title="Require Power" />
</PreferenceScreen>
<EditTextPreference
android:key="camli.max_cache_mb"
android:numeric="integer"
android:persistent="true"
android:singleLine="true"
android:title="@string/settings_max_cache_size_title" />
<EditTextPreference
android:key="camli.trusted_cert"
android:persistent="true"
android:summary="@string/settings_trusted_cert_summary"
android:title="@string/settings_trusted_cert_title" />
<EditTextPreference
android:key="camli.dev_ip"
android:phoneNumber="true"
android:persistent="true"
android:singleLine="true"
android:title="@string/settings_dev_ip" />
</PreferenceScreen>

View File

@@ -0,0 +1,506 @@
/*
* Copyright 2009 ZXing authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.zxing.integration.android;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Fragment;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
/**
* <p>A utility class which helps ease integration with Barcode Scanner via {@link Intent}s. This is a simple
* way to invoke barcode scanning and receive the result, without any need to integrate, modify, or learn the
* project's source code.</p>
*
* <h2>Initiating a barcode scan</h2>
*
* <p>To integrate, create an instance of {@code IntentIntegrator} and call {@link #initiateScan()} and wait
* for the result in your app.</p>
*
* <p>It does require that the Barcode Scanner (or work-alike) application is installed. The
* {@link #initiateScan()} method will prompt the user to download the application, if needed.</p>
*
* <p>There are a few steps to using this integration. First, your {@link Activity} must implement
* the method {@link Activity#onActivityResult(int, int, Intent)} and include a line of code like this:</p>
*
* <pre>{@code
* public void onActivityResult(int requestCode, int resultCode, Intent intent) {
* IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
* if (scanResult != null) {
* // handle scan result
* }
* // else continue with any other code you need in the method
* ...
* }
* }</pre>
*
* <p>This is where you will handle a scan result.</p>
*
* <p>Second, just call this in response to a user action somewhere to begin the scan process:</p>
*
* <pre>{@code
* IntentIntegrator integrator = new IntentIntegrator(yourActivity);
* integrator.initiateScan();
* }</pre>
*
* <p>Note that {@link #initiateScan()} returns an {@link AlertDialog} which is non-null if the
* user was prompted to download the application. This lets the calling app potentially manage the dialog.
* In particular, ideally, the app dismisses the dialog if it's still active in its {@link Activity#onPause()}
* method.</p>
*
* <p>You can use {@link #setTitle(String)} to customize the title of this download prompt dialog (or, use
* {@link #setTitleByID(int)} to set the title by string resource ID.) Likewise, the prompt message, and
* yes/no button labels can be changed.</p>
*
* <p>Finally, you can use {@link #addExtra(String, Object)} to add more parameters to the Intent used
* to invoke the scanner. This can be used to set additional options not directly exposed by this
* simplified API.</p>
*
* <p>By default, this will only allow applications that are known to respond to this intent correctly
* do so. The apps that are allowed to response can be set with {@link #setTargetApplications(List)}.
* For example, set to {@link #TARGET_BARCODE_SCANNER_ONLY} to only target the Barcode Scanner app itself.</p>
*
* <h2>Sharing text via barcode</h2>
*
* <p>To share text, encoded as a QR Code on-screen, similarly, see {@link #shareText(CharSequence)}.</p>
*
* <p>Some code, particularly download integration, was contributed from the Anobiit application.</p>
*
* <h2>Enabling experimental barcode formats</h2>
*
* <p>Some formats are not enabled by default even when scanning with {@link #ALL_CODE_TYPES}, such as
* PDF417. Use {@link #initiateScan(java.util.Collection)} with
* a collection containing the names of formats to scan for explicitly, like "PDF_417", to use such
* formats.</p>
*
* @author Sean Owen
* @author Fred Lin
* @author Isaac Potoczny-Jones
* @author Brad Drehmer
* @author gcstang
*/
public class IntentIntegrator {
public static final int REQUEST_CODE = 0x0000c0de; // Only use bottom 16 bits
private static final String TAG = IntentIntegrator.class.getSimpleName();
public static final String DEFAULT_TITLE = "Install Barcode Scanner?";
public static final String DEFAULT_MESSAGE =
"This application requires Barcode Scanner. Would you like to install it?";
public static final String DEFAULT_YES = "Yes";
public static final String DEFAULT_NO = "No";
private static final String BS_PACKAGE = "com.google.zxing.client.android";
private static final String BSPLUS_PACKAGE = "com.srowen.bs.android";
// supported barcode formats
public static final Collection<String> PRODUCT_CODE_TYPES = list("UPC_A", "UPC_E", "EAN_8", "EAN_13", "RSS_14");
public static final Collection<String> ONE_D_CODE_TYPES =
list("UPC_A", "UPC_E", "EAN_8", "EAN_13", "CODE_39", "CODE_93", "CODE_128",
"ITF", "RSS_14", "RSS_EXPANDED");
public static final Collection<String> QR_CODE_TYPES = Collections.singleton("QR_CODE");
public static final Collection<String> DATA_MATRIX_TYPES = Collections.singleton("DATA_MATRIX");
public static final Collection<String> ALL_CODE_TYPES = null;
public static final List<String> TARGET_BARCODE_SCANNER_ONLY = Collections.singletonList(BS_PACKAGE);
public static final List<String> TARGET_ALL_KNOWN = list(
BSPLUS_PACKAGE, // Barcode Scanner+
BSPLUS_PACKAGE + ".simple", // Barcode Scanner+ Simple
BS_PACKAGE // Barcode Scanner
// What else supports this intent?
);
private final Activity activity;
private final Fragment fragment;
private String title;
private String message;
private String buttonYes;
private String buttonNo;
private List<String> targetApplications;
private final Map<String,Object> moreExtras = new HashMap<String,Object>(3);
/**
* @param activity {@link Activity} invoking the integration
*/
public IntentIntegrator(Activity activity) {
this.activity = activity;
this.fragment = null;
initializeConfiguration();
}
/**
* @param fragment {@link Fragment} invoking the integration.
* {@link #startActivityForResult(Intent, int)} will be called on the {@link Fragment} instead
* of an {@link Activity}
*/
public IntentIntegrator(Fragment fragment) {
this.activity = fragment.getActivity();
this.fragment = fragment;
initializeConfiguration();
}
private void initializeConfiguration() {
title = DEFAULT_TITLE;
message = DEFAULT_MESSAGE;
buttonYes = DEFAULT_YES;
buttonNo = DEFAULT_NO;
targetApplications = TARGET_ALL_KNOWN;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public void setTitleByID(int titleID) {
title = activity.getString(titleID);
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public void setMessageByID(int messageID) {
message = activity.getString(messageID);
}
public String getButtonYes() {
return buttonYes;
}
public void setButtonYes(String buttonYes) {
this.buttonYes = buttonYes;
}
public void setButtonYesByID(int buttonYesID) {
buttonYes = activity.getString(buttonYesID);
}
public String getButtonNo() {
return buttonNo;
}
public void setButtonNo(String buttonNo) {
this.buttonNo = buttonNo;
}
public void setButtonNoByID(int buttonNoID) {
buttonNo = activity.getString(buttonNoID);
}
public Collection<String> getTargetApplications() {
return targetApplications;
}
public final void setTargetApplications(List<String> targetApplications) {
if (targetApplications.isEmpty()) {
throw new IllegalArgumentException("No target applications");
}
this.targetApplications = targetApplications;
}
public void setSingleTargetApplication(String targetApplication) {
this.targetApplications = Collections.singletonList(targetApplication);
}
public Map<String,?> getMoreExtras() {
return moreExtras;
}
public final void addExtra(String key, Object value) {
moreExtras.put(key, value);
}
/**
* Initiates a scan for all known barcode types with the default camera.
*
* @return the {@link AlertDialog} that was shown to the user prompting them to download the app
* if a prompt was needed, or null otherwise.
*/
public final AlertDialog initiateScan() {
return initiateScan(ALL_CODE_TYPES, -1);
}
/**
* Initiates a scan for all known barcode types with the specified camera.
*
* @param cameraId camera ID of the camera to use. A negative value means "no preference".
* @return the {@link AlertDialog} that was shown to the user prompting them to download the app
* if a prompt was needed, or null otherwise.
*/
public final AlertDialog initiateScan(int cameraId) {
return initiateScan(ALL_CODE_TYPES, cameraId);
}
/**
* Initiates a scan, using the default camera, only for a certain set of barcode types, given as strings corresponding
* to their names in ZXing's {@code BarcodeFormat} class like "UPC_A". You can supply constants
* like {@link #PRODUCT_CODE_TYPES} for example.
*
* @param desiredBarcodeFormats names of {@code BarcodeFormat}s to scan for
* @return the {@link AlertDialog} that was shown to the user prompting them to download the app
* if a prompt was needed, or null otherwise.
*/
public final AlertDialog initiateScan(Collection<String> desiredBarcodeFormats) {
return initiateScan(desiredBarcodeFormats, -1);
}
/**
* Initiates a scan, using the specified camera, only for a certain set of barcode types, given as strings corresponding
* to their names in ZXing's {@code BarcodeFormat} class like "UPC_A". You can supply constants
* like {@link #PRODUCT_CODE_TYPES} for example.
*
* @param desiredBarcodeFormats names of {@code BarcodeFormat}s to scan for
* @param cameraId camera ID of the camera to use. A negative value means "no preference".
* @return the {@link AlertDialog} that was shown to the user prompting them to download the app
* if a prompt was needed, or null otherwise
*/
public final AlertDialog initiateScan(Collection<String> desiredBarcodeFormats, int cameraId) {
Intent intentScan = new Intent(BS_PACKAGE + ".SCAN");
intentScan.addCategory(Intent.CATEGORY_DEFAULT);
// check which types of codes to scan for
if (desiredBarcodeFormats != null) {
// set the desired barcode types
StringBuilder joinedByComma = new StringBuilder();
for (String format : desiredBarcodeFormats) {
if (joinedByComma.length() > 0) {
joinedByComma.append(',');
}
joinedByComma.append(format);
}
intentScan.putExtra("SCAN_FORMATS", joinedByComma.toString());
}
// check requested camera ID
if (cameraId >= 0) {
intentScan.putExtra("SCAN_CAMERA_ID", cameraId);
}
String targetAppPackage = findTargetAppPackage(intentScan);
if (targetAppPackage == null) {
return showDownloadDialog();
}
intentScan.setPackage(targetAppPackage);
intentScan.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intentScan.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
attachMoreExtras(intentScan);
startActivityForResult(intentScan, REQUEST_CODE);
return null;
}
/**
* Start an activity. This method is defined to allow different methods of activity starting for
* newer versions of Android and for compatibility library.
*
* @param intent Intent to start.
* @param code Request code for the activity
* @see android.app.Activity#startActivityForResult(Intent, int)
* @see android.app.Fragment#startActivityForResult(Intent, int)
*/
protected void startActivityForResult(Intent intent, int code) {
if (fragment == null) {
activity.startActivityForResult(intent, code);
} else {
fragment.startActivityForResult(intent, code);
}
}
private String findTargetAppPackage(Intent intent) {
PackageManager pm = activity.getPackageManager();
List<ResolveInfo> availableApps = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
if (availableApps != null) {
for (String targetApp : targetApplications) {
if (contains(availableApps, targetApp)) {
return targetApp;
}
}
}
return null;
}
private static boolean contains(Iterable<ResolveInfo> availableApps, String targetApp) {
for (ResolveInfo availableApp : availableApps) {
String packageName = availableApp.activityInfo.packageName;
if (targetApp.equals(packageName)) {
return true;
}
}
return false;
}
private AlertDialog showDownloadDialog() {
AlertDialog.Builder downloadDialog = new AlertDialog.Builder(activity);
downloadDialog.setTitle(title);
downloadDialog.setMessage(message);
downloadDialog.setPositiveButton(buttonYes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
String packageName;
if (targetApplications.contains(BS_PACKAGE)) {
// Prefer to suggest download of BS if it's anywhere in the list
packageName = BS_PACKAGE;
} else {
// Otherwise, first option:
packageName = targetApplications.get(0);
}
Uri uri = Uri.parse("market://details?id=" + packageName);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
try {
if (fragment == null) {
activity.startActivity(intent);
} else {
fragment.startActivity(intent);
}
} catch (ActivityNotFoundException anfe) {
// Hmm, market is not installed
Log.w(TAG, "Google Play is not installed; cannot install " + packageName);
}
}
});
downloadDialog.setNegativeButton(buttonNo, null);
downloadDialog.setCancelable(true);
return downloadDialog.show();
}
/**
* <p>Call this from your {@link Activity}'s
* {@link Activity#onActivityResult(int, int, Intent)} method.</p>
*
* @param requestCode request code from {@code onActivityResult()}
* @param resultCode result code from {@code onActivityResult()}
* @param intent {@link Intent} from {@code onActivityResult()}
* @return null if the event handled here was not related to this class, or
* else an {@link IntentResult} containing the result of the scan. If the user cancelled scanning,
* the fields will be null.
*/
public static IntentResult parseActivityResult(int requestCode, int resultCode, Intent intent) {
if (requestCode == REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
String contents = intent.getStringExtra("SCAN_RESULT");
String formatName = intent.getStringExtra("SCAN_RESULT_FORMAT");
byte[] rawBytes = intent.getByteArrayExtra("SCAN_RESULT_BYTES");
int intentOrientation = intent.getIntExtra("SCAN_RESULT_ORIENTATION", Integer.MIN_VALUE);
Integer orientation = intentOrientation == Integer.MIN_VALUE ? null : intentOrientation;
String errorCorrectionLevel = intent.getStringExtra("SCAN_RESULT_ERROR_CORRECTION_LEVEL");
return new IntentResult(contents,
formatName,
rawBytes,
orientation,
errorCorrectionLevel);
}
return new IntentResult();
}
return null;
}
/**
* Defaults to type "TEXT_TYPE".
*
* @param text the text string to encode as a barcode
* @return the {@link AlertDialog} that was shown to the user prompting them to download the app
* if a prompt was needed, or null otherwise
* @see #shareText(CharSequence, CharSequence)
*/
public final AlertDialog shareText(CharSequence text) {
return shareText(text, "TEXT_TYPE");
}
/**
* Shares the given text by encoding it as a barcode, such that another user can
* scan the text off the screen of the device.
*
* @param text the text string to encode as a barcode
* @param type type of data to encode. See {@code com.google.zxing.client.android.Contents.Type} constants.
* @return the {@link AlertDialog} that was shown to the user prompting them to download the app
* if a prompt was needed, or null otherwise
*/
public final AlertDialog shareText(CharSequence text, CharSequence type) {
Intent intent = new Intent();
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setAction(BS_PACKAGE + ".ENCODE");
intent.putExtra("ENCODE_TYPE", type);
intent.putExtra("ENCODE_DATA", text);
String targetAppPackage = findTargetAppPackage(intent);
if (targetAppPackage == null) {
return showDownloadDialog();
}
intent.setPackage(targetAppPackage);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
attachMoreExtras(intent);
if (fragment == null) {
activity.startActivity(intent);
} else {
fragment.startActivity(intent);
}
return null;
}
private static List<String> list(String... values) {
return Collections.unmodifiableList(Arrays.asList(values));
}
private void attachMoreExtras(Intent intent) {
for (Map.Entry<String,Object> entry : moreExtras.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
// Kind of hacky
if (value instanceof Integer) {
intent.putExtra(key, (Integer) value);
} else if (value instanceof Long) {
intent.putExtra(key, (Long) value);
} else if (value instanceof Boolean) {
intent.putExtra(key, (Boolean) value);
} else if (value instanceof Double) {
intent.putExtra(key, (Double) value);
} else if (value instanceof Float) {
intent.putExtra(key, (Float) value);
} else if (value instanceof Bundle) {
intent.putExtra(key, (Bundle) value);
} else {
intent.putExtra(key, value.toString());
}
}
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2009 ZXing authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.zxing.integration.android;
/**
* <p>Encapsulates the result of a barcode scan invoked through {@link IntentIntegrator}.</p>
*
* @author Sean Owen
*/
public final class IntentResult {
private final String contents;
private final String formatName;
private final byte[] rawBytes;
private final Integer orientation;
private final String errorCorrectionLevel;
IntentResult() {
this(null, null, null, null, null);
}
IntentResult(String contents,
String formatName,
byte[] rawBytes,
Integer orientation,
String errorCorrectionLevel) {
this.contents = contents;
this.formatName = formatName;
this.rawBytes = rawBytes;
this.orientation = orientation;
this.errorCorrectionLevel = errorCorrectionLevel;
}
/**
* @return raw content of barcode
*/
public String getContents() {
return contents;
}
/**
* @return name of format, like "QR_CODE", "UPC_A". See {@code BarcodeFormat} for more format names.
*/
public String getFormatName() {
return formatName;
}
/**
* @return raw bytes of the barcode content, if applicable, or null otherwise
*/
public byte[] getRawBytes() {
return rawBytes;
}
/**
* @return rotation of the image, in degrees, which resulted in a successful scan. May be null.
*/
public Integer getOrientation() {
return orientation;
}
/**
* @return name of the error correction level used in the barcode, if applicable
*/
public String getErrorCorrectionLevel() {
return errorCorrectionLevel;
}
@Override
public String toString() {
StringBuilder dialogText = new StringBuilder(100);
dialogText.append("Format: ").append(formatName).append('\n');
dialogText.append("Contents: ").append(contents).append('\n');
int rawBytesLength = rawBytes == null ? 0 : rawBytes.length;
dialogText.append("Raw bytes: (").append(rawBytesLength).append(" bytes)\n");
dialogText.append("Orientation: ").append(orientation).append('\n');
dialogText.append("EC level: ").append(errorCorrectionLevel).append('\n');
return dialogText.toString();
}
}

View File

@@ -0,0 +1,341 @@
/*
Copyright 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.camlistore;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.MessageQueue;
import android.os.RemoteException;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
public class CamliActivity extends Activity {
private static final String TAG = "CamliActivity";
private static final int MENU_SETTINGS = 1;
private static final int MENU_STOP = 2;
private static final int MENU_STOP_DIE = 3;
private static final int MENU_UPLOAD_ALL = 4;
private static final int MENU_VERSION = 5;
private IUploadService mServiceStub = null;
private IStatusCallback mCallback = null;
// Status text update state, since it updates too quickly to do it the naive way.
private long mLastStatusUpdate = 0; // time in millis we lasted updated the screen
private String mStatusTextCurrent = null; // what the screen says
private String mStatusTextWant = null; // what the service wants it to say
private final Handler mHandler = new Handler();
private final MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
if (mStatusTextCurrent != mStatusTextWant) {
TextView textStats = (TextView) findViewById(R.id.textStats);
mLastStatusUpdate = System.currentTimeMillis();
mStatusTextCurrent = mStatusTextWant;
textStats.setText(mStatusTextWant);
}
return true;
}
};
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mServiceStub = IUploadService.Stub.asInterface(service);
Log.d(TAG, "Service connected, registering callback " + mCallback);
try {
mServiceStub.registerCallback(mCallback);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.d(TAG, "Service disconnected");
mServiceStub = null;
};
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Looper.myQueue().addIdleHandler(mIdleHandler);
final Button buttonToggle = (Button) findViewById(R.id.buttonToggle);
final TextView textStatus = (TextView) findViewById(R.id.textStatus);
final TextView textStats = (TextView) findViewById(R.id.textStats);
final TextView textBlobsRemain = (TextView) findViewById(R.id.textBlobsRemain);
final TextView textUploadStatus = (TextView) findViewById(R.id.textUploadStatus);
final ProgressBar progressBytes = (ProgressBar) findViewById(R.id.progressByteStatus);
final ProgressBar progressFile = (ProgressBar) findViewById(R.id.progressFileStatus);
buttonToggle.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View btn) {
Log.d(TAG, "button click! text=" + buttonToggle.getText());
if (getString(R.string.pause).equals(buttonToggle.getText())) {
try {
Log.d(TAG, "Pausing..");
mServiceStub.pause();
} catch (RemoteException e) {
}
} else if (getString(R.string.resume).equals(buttonToggle.getText())) {
try {
Log.d(TAG, "Resuming..");
mServiceStub.resume();
} catch (RemoteException e) {
}
}
}
});
mCallback = new IStatusCallback.Stub() {
private volatile int mLastBlobsUploadRemain = 0;
private volatile int mLastBlobsDigestRemain = 0;
@Override
public void logToClient(String stuff) throws RemoteException {
// TODO Auto-generated method stub
}
@Override
public void setUploading(final boolean uploading) throws RemoteException {
mHandler.post(new Runnable() {
@Override
public void run() {
if (uploading) {
buttonToggle.setText(R.string.pause);
textStatus.setText(R.string.uploading);
} else if (mLastBlobsDigestRemain > 0) {
buttonToggle.setText(R.string.pause);
textStatus.setText(R.string.digesting);
} else {
buttonToggle.setText(R.string.resume);
int stepsRemain = mLastBlobsUploadRemain + mLastBlobsDigestRemain;
textStatus.setText(stepsRemain > 0 ? "Paused." : "Idle.");
}
}
});
}
@Override
public void setFileStatus(final int done, final int inFlight, final int total) throws RemoteException {
mHandler.post(new Runnable() {
@Override
public void run() {
boolean finished = (done == total && mLastBlobsDigestRemain == 0);
buttonToggle.setEnabled(!finished);
progressFile.setMax(total);
progressFile.setProgress(done);
progressFile.setSecondaryProgress(done + inFlight);
if (finished) {
buttonToggle.setText(getString(R.string.pause_resume));
}
StringBuilder sb = new StringBuilder(40);
sb.append("Files to upload: ").append(total - done);
textBlobsRemain.setText(sb.toString());
}
});
}
@Override
public void setByteStatus(final long done, final int inFlight, final long total) throws RemoteException {
mHandler.post(new Runnable() {
@Override
public void run() {
// setMax takes an (signed) int, but 2GB is a totally
// reasonable upload size, so use units of 1KB instead.
progressBytes.setMax((int) (total / 1024L));
progressBytes.setProgress((int) (done / 1024L));
// TODO: renable once camput properly sends inflight information
// progressBytes.setSecondaryProgress(progressBytes.getProgress() + inFlight / 1024);
}
});
}
@Override
public void setUploadStatusText(final String text) throws RemoteException {
mHandler.post(new Runnable() {
@Override
public void run() {
textUploadStatus.setText(text);
}
});
}
@Override
public void setUploadStatsText(final String text) throws RemoteException {
// We were getting these status updates so quickly that the calls to TextView.setText
// were consuming all CPU on the main thread and it was stalling the main thread
// for seconds. Ridiculous. So instead, only update this every 5 milliseconds,
// otherwise wait for the looper to be idle to update it.
mHandler.post(new Runnable() {
@Override
public void run() {
mStatusTextWant = text;
long now = System.currentTimeMillis();
if (mLastStatusUpdate < now - 5) {
mStatusTextCurrent = mStatusTextWant;
textStats.setText(mStatusTextWant);
mLastStatusUpdate = System.currentTimeMillis();
}
}
});
}
};
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// TODO: picking files/photos to upload?
}
@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
MenuItem uploadAll = menu.add(Menu.NONE, MENU_UPLOAD_ALL, 0, R.string.upload_all);
uploadAll.setIcon(android.R.drawable.ic_menu_upload);
MenuItem stop = menu.add(Menu.NONE, MENU_STOP, 0, R.string.stop);
stop.setIcon(android.R.drawable.ic_menu_close_clear_cancel);
MenuItem stopDie = menu.add(Menu.NONE, MENU_STOP_DIE, 0, R.string.stop_die);
stopDie.setIcon(android.R.drawable.ic_menu_close_clear_cancel);
MenuItem settings = menu.add(Menu.NONE, MENU_SETTINGS, 0, R.string.settings);
settings.setIcon(android.R.drawable.ic_menu_preferences);
menu.add(Menu.NONE, MENU_VERSION, 0, R.string.version);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MENU_STOP:
try {
if (mServiceStub != null) {
mServiceStub.stopEverything();
}
} catch (RemoteException e) {
// Ignore.
}
break;
case MENU_STOP_DIE:
System.exit(1);
case MENU_SETTINGS:
SettingsActivity.show(this);
break;
case MENU_VERSION:
Toast.makeText(this, "camput version: " + ((UploadApplication) getApplication()).getCamputVersion(), Toast.LENGTH_LONG).show();
break;
case MENU_UPLOAD_ALL:
Intent uploadAll = new Intent(UploadService.INTENT_UPLOAD_ALL);
uploadAll.setClass(this, UploadService.class);
Log.d(TAG, "Starting upload all...");
startService(uploadAll);
Log.d(TAG, "Back from upload all...");
break;
}
return true;
}
@Override
protected void onPause() {
super.onPause();
try {
if (mServiceStub != null)
mServiceStub.unregisterCallback(mCallback);
} catch (RemoteException e) {
// Ignore.
}
if (mServiceConnection != null) {
unbindService(mServiceConnection);
}
}
@Override
protected void onResume() {
super.onResume();
SharedPreferences sp = getSharedPreferences(Preferences.NAME, 0);
try {
HostPort hp = new HostPort(sp.getString(Preferences.HOST, ""));
if (!hp.isValid()) {
// Crashes oddly in some Android Instrumentation thing if
// uncommented:
// SettingsActivity.show(this);
// return;
}
} catch (NumberFormatException enf) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage("Server should be of form [https://]<host[:port]>")
.setTitle("Invalid Setting");
AlertDialog alert = builder.create();
alert.show();
}
bindService(new Intent(this, UploadService.class), mServiceConnection, Context.BIND_AUTO_CREATE);
Intent intent = getIntent();
String action = intent.getAction();
Log.d(TAG, "onResume; action=" + action);
if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
Intent serviceIntent = new Intent(intent);
serviceIntent.setClass(this, UploadService.class);
startService(serviceIntent);
setIntent(new Intent(this, CamliActivity.class));
} else {
Log.d(TAG, "Normal CamliActivity viewing.");
}
}
}

View File

@@ -0,0 +1,65 @@
/*
Copyright 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.camlistore;
import java.io.File;
import android.net.Uri;
import android.os.FileObserver;
import android.os.RemoteException;
import android.util.Log;
import org.camlistore.IUploadService.Stub;
public class CamliFileObserver extends FileObserver {
private static final String TAG = "CamliFileObserver";
private final File mDirectory;
private final Stub mServiceStub;
public CamliFileObserver(IUploadService.Stub service, File directory) {
super(directory.getAbsolutePath(), FileObserver.CLOSE_WRITE | FileObserver.MOVED_TO);
// TODO: Docs say: "The monitored file or directory must exist at this
// time, or else no events will be reported (even if it appears
// later).". This means that a user without, say, a "gpx/" directory
// that then goes to "Export all Tracks.." won't start them uploading.
mDirectory = directory;
mServiceStub = service;
Log.d(TAG, "Starting to watch: " + mDirectory.getAbsolutePath());
startWatching();
}
@Override
public void onEvent(int event, String path) {
if (path == null) {
// It's null for certain directory-level events.
return;
}
// Note from docs:
// "This method is invoked on a special FileObserver thread."
// Order in which we get events for a new camera picture:
// CREATE, OPEN, MODIFY, [OPEN, CLOSE_NOWRITE], CLOSE_WRITE
File fullFile = new File(mDirectory, path);
Log.d(TAG, "event " + event + " for " + fullFile.getAbsolutePath());
try {
mServiceStub.enqueueUpload(Uri.fromFile(fullFile));
} catch (RemoteException e) {
}
}
}

View File

@@ -0,0 +1,56 @@
/*
Copyright 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.camlistore;
import android.os.RemoteException;
/**
* No-op callback for service to use when it doesn't have a real callback.
* Avoids a lot of null checks.
*/
public class DummyNullCallback extends IStatusCallback.Stub {
private static final IStatusCallback.Stub mInstance = new DummyNullCallback();
public static IStatusCallback.Stub instance() {
return mInstance;
}
@Override
public void logToClient(String stuff) throws RemoteException {
}
@Override
public void setByteStatus(long done, int inFlight, long total) throws RemoteException {
}
@Override
public void setUploading(boolean uploading) throws RemoteException {
}
@Override
public void setUploadStatusText(String text) throws RemoteException {
}
@Override
public void setFileStatus(int done, int inFlight, int total) throws RemoteException {
}
@Override
public void setUploadStatsText(String text) throws RemoteException {
}
}

View File

@@ -0,0 +1,103 @@
/*
Copyright 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.camlistore;
/**
* HostPort parses a "host.com", "host.com:port", or "https://host.com:port"
* It doesn't handle paths. TODO(bradfitz): This should probably be scrapped
* and use a URL parser or something instead.
*/
public class HostPort {
private final boolean mValid;
private final String mHost;
private final int mPort;
private final boolean mSecure;
private static final String HTTP_PREFIX = "http://";
private static final String SECURE_PREFIX = "https://";
public HostPort(String hostPort) {
if (hostPort.startsWith(HTTP_PREFIX)) {
mSecure = false;
hostPort = hostPort.substring(HTTP_PREFIX.length());
} else if (hostPort.startsWith(SECURE_PREFIX)) {
mSecure = true;
hostPort = hostPort.substring(SECURE_PREFIX.length());
} else {
mSecure = false;
}
String[] parts = hostPort.split(":");
if (parts.length == 2) {
mHost = parts[0];
mPort = new Integer(parts[1]).intValue();
mValid = true;
} else if (parts.length > 2 || parts.length == 0) {
mValid = false;
mHost = null;
mPort = 0;
} else {
mValid = hostPort.length() > 0;
mHost = hostPort;
mPort = mSecure ? 443 : 80;
}
}
public int port() {
return mPort;
}
public String host() {
return mHost;
}
public boolean isValid() {
return mValid;
}
public boolean isSecure() {
return mSecure;
}
private boolean nonStandardPort() {
return mPort != (mSecure ? 443 : 80);
}
public String urlPrefix() {
StringBuilder sb = new StringBuilder(12 + mHost.length());
sb.append(httpScheme());
sb.append("://");
sb.append(mHost);
if (nonStandardPort()) {
sb.append(":");
sb.append(mPort);
}
return sb.toString();
}
public String httpScheme() {
return mSecure ? "https" : "http";
}
@Override
public String toString() {
if (!mValid) {
return "[invalid HostPort]";
}
return mHost + ":" + mPort;
}
}

View File

@@ -0,0 +1,30 @@
/*
Copyright 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.camlistore;
oneway interface IStatusCallback {
void logToClient(String stuff);
void setUploadStatusText(String text); // single line
void setUploadStatsText(String text); // big box
void setUploading(boolean uploading);
// done: acknowledged by server
// inFlight: those written to the server, but no reply yet (i.e. large HTTP POST body) (does NOT include the "done" ones)
// total: "this batch" size. reset on transition from 0 -> 1 blobs remain.
void setFileStatus(int done, int inFlight, int total);
void setByteStatus(long done, int inFlight, long total);
}

View File

@@ -0,0 +1,49 @@
/*
Copyright 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.camlistore;
import org.camlistore.IStatusCallback;
import android.os.ParcelFileDescriptor;
import android.net.Uri;
import java.util.List;
interface IUploadService {
void registerCallback(IStatusCallback cb);
void unregisterCallback(IStatusCallback cb);
int queueSize();
boolean isUploading();
// Returns true if thread was running and we requested it be stopped.
boolean pause();
// Returns true if upload wasn't already in progress and new upload
// thread was started.
boolean resume();
// Enqueues a new file to be uploaded (a file:// or content:// URI). Does disk I/O,
// so should be called from an AsyncTask.
// Returns false if server not configured.
boolean enqueueUpload(in Uri uri);
int enqueueUploadList(in List<Uri> uri);
// Stop stop uploads, clear queues.
void stopEverything();
// For the SettingsActivity
void setBackgroundWatchersEnabled(boolean enabled);
}

View File

@@ -0,0 +1,32 @@
/*
Copyright 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.camlistore;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class OnAlarmReceiver extends BroadcastReceiver {
private static final String TAG = "Camli_OnAlarmReceiver";
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "alarm");
}
}

View File

@@ -0,0 +1,43 @@
/*
Copyright 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.camlistore;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.SystemClock;
import android.util.Log;
public class OnBootReceiver extends BroadcastReceiver {
private static final String TAG = "Camli_OnBootReceiver";
@Override
public void onReceive(Context context, Intent intent) {
Log.v(TAG, "onReceive on boot");
AlarmManager alarmer = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, new Intent(context,
OnAlarmReceiver.class), 0);
alarmer.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + 60000, AlarmManager.INTERVAL_HALF_HOUR,
pendingIntent);
}
}

View File

@@ -0,0 +1,114 @@
/*
Copyright 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.camlistore;
import android.content.SharedPreferences;
public final class Preferences {
public static final String NAME = "CamliUploader";
public static final String HOST = "camli.host";
// TODO(mpl): list instead of single string later? seems overkill for now.
public static final String TRUSTED_CERT = "camli.trusted_cert";
public static final String USERNAME = "camli.username";
public static final String PASSWORD = "camli.password";
public static final String AUTO = "camli.auto";
public static final String AUTO_OPTS = "camli.auto.opts";
public static final String MAX_CACHE_MB = "camli.max_cache_mb";
public static final String DEV_IP = "camli.dev_ip";
public static final String AUTO_REQUIRE_POWER = "camli.auto.require_power";
public static final String AUTO_REQUIRE_WIFI = "camli.auto.require_wifi";
public static final String AUTO_REQUIRED_WIFI_SSID = "camli.auto.required_wifi_ssid";
public static final String AUTO_DIR_PHOTOS = "camli.auto.photos";
public static final String AUTO_DIR_MYTRACKS = "camli.auto.mytracks";
private final SharedPreferences mSP;
public Preferences(SharedPreferences prefs) {
mSP = prefs;
}
public boolean autoRequiresPower() {
return mSP.getBoolean(AUTO_REQUIRE_POWER, false);
}
public boolean autoRequiresWifi() {
return mSP.getBoolean(AUTO_REQUIRE_WIFI, false);
}
public String autoRequiredWifiSSID() {
return mSP.getString(AUTO_REQUIRED_WIFI_SSID, "");
}
public boolean autoUpload() {
return mSP.getBoolean(AUTO, false);
}
public int maxCacheMb() {
return Integer.parseInt(mSP.getString(MAX_CACHE_MB, "256"));
}
public long maxCacheBytes() {
return maxCacheMb() * 1024 * 1024;
}
public boolean autoDirPhotos() {
return mSP.getBoolean(AUTO_DIR_PHOTOS, true);
}
public boolean autoDirMyTracks() {
return mSP.getBoolean(AUTO_DIR_MYTRACKS, true);
}
private String devIP() {
return mSP.getString(DEV_IP, "");
}
private boolean inDevMode() {
return !devIP().isEmpty();
}
public String username() {
if (inDevMode()) {
return "camlistore";
}
return mSP.getString(USERNAME, "");
}
public String password() {
if (inDevMode()) {
return "pass3179";
}
return mSP.getString(PASSWORD, "");
}
public HostPort hostPort() {
if (inDevMode()) {
return new HostPort("http://" + devIP() + ":3179");
}
return new HostPort(mSP.getString(Preferences.HOST, ""));
}
public String trustedCert() {
return mSP.getString(TRUSTED_CERT, "").toLowerCase();
}
public void setDevIP(String value) {
mSP.edit().putString(DEV_IP, value).apply();
}
}

View File

@@ -0,0 +1,31 @@
package org.camlistore;
import android.app.AlertDialog;
import android.content.Context;
import android.preference.Preference;
import android.util.AttributeSet;
import android.util.Log;
import com.google.zxing.integration.android.IntentIntegrator;
/**
* QRPrefence implements a custom {@link Preference} for scanning barcodes.
*
* It will launch a barcode scanner intent configured for scanning QR codes using {@link IntentIntegrator}. If no barcode scanner app is installed {@link IntentIntegrator} will prompt the user to install one from the Google Play market.
*/
public class QRPreference extends Preference {
private static final String TAG = "QRPreference";
public QRPreference(Context context, AttributeSet attrs) {
super(context, attrs);
Log.v(TAG, "QRPreference");
}
@Override
protected void onClick() {
SettingsActivity activity = (SettingsActivity) this.getContext();
IntentIntegrator integrator = new IntentIntegrator(activity);
integrator.initiateScan(IntentIntegrator.QR_CODE_TYPES);
}
}

View File

@@ -0,0 +1,84 @@
/*
Copyright 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.camlistore;
import android.net.Uri;
/**
* Immutable struct for tuple (sha1 blobRef, URI to upload, size of blob).
*/
public class QueuedFile {
private final Uri mUri;
private final long mSize;
private final String mDiskPath; // or null if it can't be resolved.
public QueuedFile(Uri uri, long size, String diskPath) {
if (uri == null) {
throw new NullPointerException("uri == null");
}
mUri = uri;
mSize = size;
mDiskPath = diskPath;
}
public Uri getUri() {
return mUri;
}
public long getSize() {
return mSize;
}
// getDiskPath may return null, if the URI couldn't be resolved to a path on disk.
public String getDiskPath() {
return mDiskPath;
}
@Override
public String toString() {
return "QueuedFile [mSize=" + mSize + ", mUri=" + mUri + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (int) (mSize ^ (mSize >>> 32));
result = prime * result + ((mUri == null) ? 0 : mUri.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
QueuedFile other = (QueuedFile) obj;
if (mSize != other.mSize)
return false;
if (mUri == null) {
if (other.mUri != null)
return false;
} else if (!mUri.equals(other.mUri))
return false;
return true;
}
}

View File

@@ -0,0 +1,355 @@
/*
Copyright 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.camlistore;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import android.app.AlertDialog;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.net.Uri;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.PreferenceActivity;
import android.preference.PreferenceScreen;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
public class SettingsActivity extends PreferenceActivity {
private static final String TAG = "SettingsActivity";
private IUploadService mServiceStub = null;
private EditTextPreference hostPref;
private EditTextPreference trustedCertPref;
private EditTextPreference usernamePref;
private EditTextPreference passwordPref;
private EditTextPreference devIPPref;
private CheckBoxPreference autoPref;
private PreferenceScreen autoOpts;
private EditTextPreference maxCacheSizePref;
private SharedPreferences mSharedPrefs;
private Preferences mPrefs;
private Map<CharSequence, String> prefToParam;
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mServiceStub = IUploadService.Stub.asInterface(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
mServiceStub = null;
};
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Map<CharSequence, String> m = new HashMap<CharSequence, String>();
m.put(Preferences.HOST, "server");
m.put(Preferences.TRUSTED_CERT, "certFingerprint");
m.put(Preferences.USERNAME, "username");
m.put(Preferences.PASSWORD, "password");
m.put(Preferences.AUTO, "autoUpload");
m.put(Preferences.MAX_CACHE_MB, "maxCacheSize");
prefToParam = Collections.unmodifiableMap(m);
getPreferenceManager().setSharedPreferencesName(Preferences.NAME);
addPreferencesFromResource(R.xml.preferences);
hostPref = (EditTextPreference) findPreference(Preferences.HOST);
// TODO(mpl): popup window that proposes to automatically add the cert to
// the prefs when we fail to dial an untrusted server (and only in that case).
trustedCertPref = (EditTextPreference) findPreference(Preferences.TRUSTED_CERT);
usernamePref = (EditTextPreference) findPreference(Preferences.USERNAME);
passwordPref = (EditTextPreference) findPreference(Preferences.PASSWORD);
autoPref = (CheckBoxPreference) findPreference(Preferences.AUTO);
autoOpts = (PreferenceScreen) findPreference(Preferences.AUTO_OPTS);
maxCacheSizePref = (EditTextPreference) findPreference(Preferences.MAX_CACHE_MB);
devIPPref = (EditTextPreference) findPreference(Preferences.DEV_IP);
mSharedPrefs = getSharedPreferences(Preferences.NAME, 0);
mPrefs = new Preferences(mSharedPrefs);
// Display defaults.
maxCacheSizePref.setSummary(getString(
R.string.settings_max_cache_size_summary, mPrefs.maxCacheMb()));
OnPreferenceChangeListener onChange = new OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference pref, Object newValue) {
final String key = pref.getKey();
Log.v(TAG, "preference change for: " + key);
// Note: newValue isn't yet persisted, but easiest to update the
// UI here.
String newStr = (newValue instanceof String) ? (String) newValue
: null;
if (pref == hostPref) {
updateHostSummary(newStr);
} else if (pref == trustedCertPref) {
updateTrustedCertSummary(newStr);
} else if (pref == passwordPref) {
updatePasswordSummary(newStr);
} else if (pref == usernamePref) {
updateUsernameSummary(newStr);
} else if (pref == maxCacheSizePref) {
if (!updateMaxCacheSizeSummary(newStr))
return false;
} else if (pref == devIPPref) {
updateDevIP(newStr);
}
return true; // yes, persist it
}
};
hostPref.setOnPreferenceChangeListener(onChange);
trustedCertPref.setOnPreferenceChangeListener(onChange);
passwordPref.setOnPreferenceChangeListener(onChange);
usernamePref.setOnPreferenceChangeListener(onChange);
maxCacheSizePref.setOnPreferenceChangeListener(onChange);
devIPPref.setOnPreferenceChangeListener(onChange);
}
/**
* Receives the results from the custome QRPreference's call to the barcode scanner intent.
*
* This is never called if the user doesn't have a zxing barcode scanner app installed.
*/
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
if (scanResult != null && scanResult.getContents() != null) {
// handle scan result
Log.v(TAG, "Scan result" + scanResult);
Uri uri = Uri.parse(scanResult.getContents());
confirmNewSettingsDialog(uri);
} else {
// else continue with any other code you need in the method
Log.v(TAG, "No result");
}
}
/**
* confirmNewSettingsDialog will set preferences based on the parameters
* in uri.
*
* It is expected the schema of uri is 'camli' and the host is 'settings'.
* Uri parameters expected are server, certFingerprint, username,
* autoUpload, maxCacheSize, and password
*/
private final void confirmNewSettingsDialog(final Uri uri) {
Log.v(TAG, "QR resolved to: " + uri);
if (!(uri.getScheme().equals("camli") && uri.getHost().equals("settings"))) {
Toast.makeText(this, "QR code not a camli://settings/ URL", Toast.LENGTH_LONG).show();
return;
}
AlertDialog.Builder builder = new AlertDialog.Builder(this);
StringBuilder confirmation = new StringBuilder();
for (Map.Entry<CharSequence, String> pref : prefToParam.entrySet()) {
confirmation.append(findPreference(pref.getKey()).getTitle());
confirmation.append(": ");
confirmation.append(uri.getQueryParameter(pref.getValue()));
confirmation.append("\n");
}
builder.setMessage(confirmation.toString())
.setTitle(R.string.settings_confirmation_dialog_title);
builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
hostPref.setText(uri.getQueryParameter("server"));
trustedCertPref.setText(uri.getQueryParameter("certFingerprint"));
usernamePref.setText(uri.getQueryParameter("username"));
String auto = uri.getQueryParameter("autoUpload");
autoPref.setChecked(auto != null && auto.equals("1"));
maxCacheSizePref.setText(uri.getQueryParameter("maxCacheSize"));
// Password isn't a value that can be set on /ui/mobile.html. It
// seems like a security risk to do so. If there's a smart way to do it,
// I'm up for suggestions. In the meantime, if a person manually
// adds it to the QR code URL, use it (helpful during development)
// -wathiede.
passwordPref.setText(uri.getQueryParameter("password"));
updatePreferenceSummaries();
}
});
builder.setNegativeButton(R.string.cancel, null);
builder.create().show();
}
private final SharedPreferences.OnSharedPreferenceChangeListener prefChangedHandler = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sp, String key) {
if (Preferences.AUTO.equals(key)) {
boolean val = mPrefs.autoUpload();
updateAutoOpts(val);
Log.d(TAG, "AUTO changed to " + val);
if (mServiceStub != null) {
try {
mServiceStub.setBackgroundWatchersEnabled(val);
} catch (RemoteException e) {
// Ignore.
}
}
}
}
};
@Override
protected void onPause() {
super.onPause();
mSharedPrefs
.unregisterOnSharedPreferenceChangeListener(prefChangedHandler);
if (mServiceConnection != null) {
unbindService(mServiceConnection);
}
}
@Override
protected void onResume() {
super.onResume();
updatePreferenceSummaries();
mSharedPrefs
.registerOnSharedPreferenceChangeListener(prefChangedHandler);
bindService(new Intent(this, UploadService.class), mServiceConnection,
Context.BIND_AUTO_CREATE);
}
private void updatePreferenceSummaries() {
updateHostSummary(hostPref.getText());
updateTrustedCertSummary(trustedCertPref.getText());
updatePasswordSummary(passwordPref.getText());
updateAutoOpts(autoPref.isChecked());
updateMaxCacheSizeSummary(maxCacheSizePref.getText());
updateUsernameSummary(usernamePref.getText());
updateDevIP(devIPPref.getText());
}
private void updateDevIP(String value) {
// The Brad-is-lazy shortcut: if the user enters "12", assumes
// "10.0.0.12", or whatever
// the current wifi connections's /24 is.
if (!TextUtils.isEmpty(value) && !value.contains(".")) {
WifiManager wifiManager = (WifiManager) getSystemService(WIFI_SERVICE);
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
if (wifiInfo != null) {
int ip = wifiInfo.getIpAddress();
value = String.format("%d.%d.%d.", ip & 0xff, (ip >> 8) & 0xff,
(ip >> 16) & 0xff) + value;
devIPPref.setText(value);
mPrefs.setDevIP(value);
}
}
boolean enabled = TextUtils.isEmpty(value);
hostPref.setEnabled(enabled);
trustedCertPref.setEnabled(enabled);
usernamePref.setEnabled(enabled);
passwordPref.setEnabled(enabled);
if (!enabled) {
devIPPref.setSummary("Using http://" + value
+ ":3179 user/pass \"camlistore\", \"pass3179\"");
} else {
devIPPref.setSummary("(Dev-server IP to override settings above)");
}
}
private void updatePasswordSummary(String value) {
if (value != null && value.length() > 0) {
passwordPref.setSummary("*********");
} else {
passwordPref.setSummary("<unset>");
}
}
private void updateUsernameSummary(String value) {
if (value != null && value.length() > 0) {
usernamePref.setSummary(value);
} else {
usernamePref.setSummary("<unset>");
}
}
private void updateHostSummary(String value) {
if (value != null && value.length() > 0) {
hostPref.setSummary(value);
} else {
hostPref.setSummary(getString(R.string.settings_host_summary));
}
}
private void updateTrustedCertSummary(String value) {
if (value != null && value.length() > 0) {
trustedCertPref.setSummary(value);
} else {
trustedCertPref.setSummary("<unset; optional 20 hex SHA-256 prefix>");
}
}
private void updateAutoOpts(boolean checked) {
autoOpts.setEnabled(checked);
}
// Update the summary for the max cache size setting.
// Returns true if the value is valid and should be persisted and false
// otherwise.
private boolean updateMaxCacheSizeSummary(String value) {
try {
int mb = Integer.parseInt(value);
if (mb <= 0)
return false;
maxCacheSizePref.setSummary(getString(
R.string.settings_max_cache_size_summary, mb));
return true;
} catch (NumberFormatException e) {
return false;
}
}
// Convenience method.
static void show(Context context) {
final Intent intent = new Intent(context, SettingsActivity.class);
context.startActivity(intent);
}
}

View File

@@ -0,0 +1,128 @@
/*
Copyright 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.camlistore;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import android.app.Application;
import android.content.pm.PackageManager.NameNotFoundException;
import android.util.Log;
public class UploadApplication extends Application {
private final static String TAG = "UploadApplication";
private final static boolean STRICT_MODE = true;
private long getAPKModTime() {
try {
return getPackageManager().getPackageInfo(getPackageName(), 0).lastUpdateTime;
} catch (NameNotFoundException e) {
throw new RuntimeException(e);
}
}
private void copyGoBinary() {
long myTime = getAPKModTime();
String dstFile = getBaseContext().getFilesDir().getAbsolutePath() + "/camput.bin";
File f = new File(dstFile);
Log.d(TAG, " My Time: " + myTime);
Log.d(TAG, "Bin Time: " + f.lastModified());
if (f.exists() && f.lastModified() > myTime) {
Log.d(TAG, "Go binary modtime up-to-date.");
return;
}
Log.d(TAG, "Go binary missing or modtime stale. Re-copying from APK.");
try {
InputStream is = getAssets().open("camput.arm");
FileOutputStream fos = getBaseContext().openFileOutput("camput.bin.writing", MODE_PRIVATE);
byte[] buf = new byte[8192];
int offset;
while ((offset = is.read(buf)) > 0) {
fos.write(buf, 0, offset);
}
is.close();
fos.flush();
fos.close();
String writingFilePath = dstFile + ".writing";
Log.d(TAG, "wrote out " + writingFilePath);
Runtime.getRuntime().exec("chmod 0777 " + writingFilePath);
Log.d(TAG, "did chmod 0700 on " + writingFilePath);
Runtime.getRuntime().exec("mv " + writingFilePath + " " + dstFile);
f = new File(dstFile);
f.setLastModified(myTime);
Log.d(TAG, "set modtime of " + dstFile);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void onCreate() {
super.onCreate();
copyGoBinary();
if (!STRICT_MODE) {
Log.d(TAG, "Starting UploadApplication; release build.");
return;
}
try {
Runtime.getRuntime().exec("chmod 0755 " + getCacheDir().getAbsolutePath());
} catch (IOException e) {
Log.d(TAG, "failed to chmod cache dir");
}
try {
Class strictmode = Class.forName("android.os.StrictMode");
Log.d(TAG, "StrictMode class found.");
Method method = strictmode.getMethod("enableDefaults");
Log.d(TAG, "enableDefaults method found.");
method.invoke(null);
} catch (ClassNotFoundException e) {
} catch (LinkageError e) {
} catch (IllegalAccessException e) {
} catch (NoSuchMethodException e) {
} catch (SecurityException e) {
} catch (java.lang.reflect.InvocationTargetException e) {
}
}
public String getCamputVersion() {
InputStream is = null;
try {
is = getAssets().open("camput-version.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
return br.readLine();
} catch (IOException e) {
return e.toString();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
}
}
}
}
}

View File

@@ -0,0 +1,846 @@
/*
Copyright 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.camlistore;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import org.camlistore.UploadThread.CamputChunkUploadedMessage;
import org.camlistore.UploadThread.CamputStatsMessage;
import android.app.Notification;
import android.app.Notification.Builder;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.Environment;
import android.os.FileObserver;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
import android.os.PowerManager;
import android.os.RemoteException;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.Toast;
public class UploadService extends Service {
private static final String TAG = "UploadService";
private static int NOTIFY_ID_UPLOADING = 0x001;
public static final String INTENT_POWER_CONNECTED = "POWER_CONNECTED";
public static final String INTENT_POWER_DISCONNECTED = "POWER_DISCONNECTED";
public static final String INTENT_UPLOAD_ALL = "UPLOAD_ALL";
public static final String INTENT_NETWORK_WIFI = "WIFI_NOW";
public static final String INTENT_NETWORK_NOT_WIFI = "NOT_WIFI_NOW";
// Everything in this block guarded by 'this':
private boolean mUploading = false; // user's desired state (notified
// quickly)
private UploadThread mUploadThread = null; // last thread created; null when
// thread exits
private Notification.Builder mNotificationBuilder; // null until upload is
// started/resumed
private final Map<QueuedFile, Long> mFileBytesRemain = new HashMap<QueuedFile, Long>();
private final LinkedList<QueuedFile> mQueueList = new LinkedList<QueuedFile>();
private final Map<String, Long> mStatValue = new TreeMap<String, Long>();
private IStatusCallback mCallback = DummyNullCallback.instance();
private String mLastUploadStatusText = null; // single line
private String mLastUploadStatsText = null; // multi-line stats
private int mBytesInFlight = 0;
private int mFilesInFlight = 0;
// Stats, all guarded by 'this', and all reset to 0 on queue size transition
// from 0 -> 1.
private long mBytesTotal = 0;
private long mBytesUploaded = 0;
private int mFilesTotal = 0;
private int mFilesUploaded = 0;
// Effectively final, initialized in onCreate():
PowerManager mPowerManager;
WifiManager mWifiManager;
NotificationManager mNotificationManager;
Preferences mPrefs;
// File Observers. Need to keep a reference to them, as there's no JNI
// reference and their finalizers would run otherwise, stopping their
// inotify.
private final ArrayList<FileObserver> mObservers = new ArrayList<FileObserver>();
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate");
mPowerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
mWifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
mPrefs = new Preferences(getSharedPreferences(Preferences.NAME, 0));
updateBackgroundWatchers();
}
@Override
public IBinder onBind(Intent intent) {
Log.d(TAG, "onBind intent=" + intent);
return service;
}
@Override
public void onStart(Intent intent, int startId) {
handleCommand(intent);
}
private void startUploadService() {
startService(new Intent(UploadService.this, UploadService.class));
}
// This is @Override as of SDK version 5, but we're targetting 4 (Android
// 1.6)
private static final int START_STICKY = 1; // in SDK 5
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
handleCommand(intent);
// We want this service to continue running until it is explicitly
// stopped, so return sticky.
return START_STICKY;
}
private void handleCommand(Intent intent) {
Log.d(TAG, "in handleCommand() for onStart() intent: " + intent);
if (intent == null) {
stopServiceIfEmpty();
return;
}
String action = intent.getAction();
if (Intent.ACTION_SEND.equals(action)) {
handleSend(intent);
stopServiceIfEmpty();
return;
}
if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
handleSendMultiple(intent);
stopServiceIfEmpty();
return;
}
if (INTENT_UPLOAD_ALL.equals(action)) {
handleUploadAll();
return;
}
try {
if (mPrefs.autoUpload()) {
boolean startAuto = false;
boolean stopAuto = false;
if (INTENT_POWER_CONNECTED.equals(action)) {
if (!mPrefs.autoRequiresWifi() || WifiPowerReceiver.onWifi(this)) {
startAuto = true;
}
} else if (INTENT_NETWORK_WIFI.equals(action)) {
if (!mPrefs.autoRequiresPower() || WifiPowerReceiver.onPower(this)) {
String ssid = "";
String requiredSSID = mPrefs.autoRequiredWifiSSID();
if (intent.hasExtra("SSID")) {
ssid = intent.getStringExtra("SSID");
}
Log.d(TAG, "SSID: '" + ssid +"' / Required SSID: '" + requiredSSID + "'");
if (requiredSSID.equals("") || requiredSSID.equals(ssid)) {
startAuto = true;
}
}
} else if (INTENT_POWER_DISCONNECTED.equals(action)) {
stopAuto = mPrefs.autoRequiresPower();
} else if (INTENT_NETWORK_NOT_WIFI.equals(action)) {
stopAuto = mPrefs.autoRequiresWifi();
}
if (startAuto) {
Log.d(TAG, "Starting automatic uploads");
service.resume();
handleUploadAll();
return;
}
if (stopAuto) {
Log.d(TAG, "Stopping automatic uploads");
service.pause();
stopBackgroundWatchers();
stopServiceIfEmpty();
return;
}
}
} catch (RemoteException e) {
// Ignore.
}
}
private void handleSend(Intent intent) {
Bundle extras = intent.getExtras();
if (extras == null) {
Log.w(TAG, "expected extras in handleSend");
return;
}
extras.keySet(); // unparcel
Log.d(TAG, "handleSend; extras=" + extras);
Object streamValue = extras.get("android.intent.extra.STREAM");
if (!(streamValue instanceof Uri)) {
Log.w(TAG, "Expected URI for STREAM; got: " + streamValue);
return;
}
final Uri uri = (Uri) streamValue;
Util.runAsync(new Runnable() {
@Override
public void run() {
try {
service.enqueueUpload(uri);
} catch (RemoteException e) {
} finally {
stopServiceIfEmpty();
}
}
});
}
private void handleUploadAll() {
startService(new Intent(UploadService.this, UploadService.class));
final PowerManager.WakeLock wakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Camli Upload All");
wakeLock.acquire();
Util.runAsync(new Runnable() {
@Override
public void run() {
try {
List<String> dirs = getBackupDirs();
List<Uri> filesToQueue = new ArrayList<Uri>();
for (String dirName : dirs) {
File dir = new File(dirName);
if (!dir.exists()) {
continue;
}
File[] files = dir.listFiles();
if (files != null) {
for (int i = 0; i < files.length; ++i) {
File f = files[i];
if (f.isDirectory()) {
// Skip thumbnails directory.
// TODO: are any interesting enough to recurse into?
// Definitely don't need to upload thumbnails, but
// but maybe some other app in the the future creates
// sharded directories. Eye-Fi doesn't, though.
continue;
}
filesToQueue.add(Uri.fromFile(f));
}
}
}
try {
service.enqueueUploadList(filesToQueue);
} catch (RemoteException e) {
} finally {
stopServiceIfEmpty();
}
} finally {
wakeLock.release();
}
}
});
}
private List<String> getBackupDirs() {
ArrayList<String> dirs = new ArrayList<String>();
if (mPrefs.autoDirPhotos()) {
dirs.add(Environment.getExternalStorageDirectory() + "/DCIM/Camera");
dirs.add(Environment.getExternalStorageDirectory() + "/DCIM/100MEDIA");
dirs.add(Environment.getExternalStorageDirectory() + "/DCIM/100ANDRO");
dirs.add(Environment.getExternalStorageDirectory() + "/Eye-Fi");
}
if (mPrefs.autoDirMyTracks()) {
dirs.add(Environment.getExternalStorageDirectory() + "/gpx");
dirs.add(Environment.getExternalStorageDirectory() + "/kml");
}
return dirs;
}
private void handleSendMultiple(Intent intent) {
ArrayList<Parcelable> items = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
ArrayList<Uri> uris = new ArrayList<Uri>(items.size());
for (Parcelable p : items) {
if (!(p instanceof Uri)) {
Log.d(TAG, "uh, unknown thing " + p);
continue;
}
uris.add((Uri) p);
}
final ArrayList<Uri> finalUris = uris;
Util.runAsync(new Runnable() {
@Override
public void run() {
try {
service.enqueueUploadList(finalUris);
} catch (RemoteException e) {
} finally {
stopServiceIfEmpty();
}
}
});
}
private void stopBackgroundWatchers() {
synchronized (UploadService.this) {
if (mObservers.isEmpty()) {
return;
}
Log.d(TAG, "Stopping background watchers...");
for (FileObserver fo : mObservers) {
fo.stopWatching();
}
mObservers.clear();
}
}
private void updateBackgroundWatchers() {
stopBackgroundWatchers();
if (!mPrefs.autoUpload()) {
return;
}
startBackgroundWatchers();
}
private void startBackgroundWatchers() {
Log.d(TAG, "Starting background watchers...");
synchronized (UploadService.this) {
maybeAddObserver("DCIM/Camera");
maybeAddObserver("DCIM/100MEDIA");
maybeAddObserver("DCIM/100ANDRO");
maybeAddObserver("Eye-Fi");
maybeAddObserver("gpx");
}
}
// Requires that UploadService.this is locked.
private void maybeAddObserver(String suffix) {
File f = new File(Environment.getExternalStorageDirectory(), suffix);
if (f.exists()) {
mObservers.add(new CamliFileObserver(service, f));
}
}
@Override
public void onDestroy() {
synchronized (this) {
Log.d(TAG, "onDestroy of camli UploadService; thread=" + mUploadThread + "; uploading=" + mUploading + "; queue size=" + mFileBytesRemain.size());
}
super.onDestroy();
if (mUploadThread != null) {
Log.e(TAG, "Unexpected onDestroy with active upload thread. Killing it.");
mUploadThread.interrupt();
mUploadThread = null;
}
}
// Called by UploadThread to get stuff to do. Caller owns the returned new
// LinkedList. Doesn't return null.
LinkedList<QueuedFile> uploadQueue() {
synchronized (this) {
LinkedList<QueuedFile> copy = new LinkedList<QueuedFile>();
copy.addAll(mQueueList);
return copy;
}
}
void setUploadStatusText(String status) {
IStatusCallback cb;
synchronized (this) {
mLastUploadStatusText = status;
cb = mCallback;
}
try {
cb.setUploadStatusText(status);
} catch (RemoteException e) {
}
}
void setInFlightBytes(int v) {
synchronized (this) {
mBytesInFlight = v;
}
broadcastByteStatus();
}
void broadcastByteStatus() {
Notification notification = null;
synchronized (this) {
if (mNotificationBuilder != null) {
int kBUploaded = (int)(mBytesUploaded / 1024L);
int kBTotal = (int)(mBytesTotal / 1024L);
mNotificationBuilder.setProgress(kBTotal, kBUploaded, false);
notification = mNotificationBuilder.build();
}
try {
mCallback.setByteStatus(mBytesUploaded, mBytesInFlight, mBytesTotal);
} catch (RemoteException e) {
}
}
if (notification != null) {
mNotificationManager.notify(NOTIFY_ID_UPLOADING, notification);
}
}
void broadcastFileStatus() {
// TODO read mfiles/mcallback under lock and setfilestatus after lock
synchronized (this) {
try {
mCallback.setFileStatus(mFilesUploaded, mFilesInFlight, mFilesTotal);
} catch (RemoteException e) {
}
}
}
void broadcastAllState() {
synchronized (this) {
try {
mCallback.setUploading(mUploading);
mCallback.setUploadStatusText(mLastUploadStatusText);
mCallback.setUploadStatsText(mLastUploadStatsText);
} catch (RemoteException e) {
}
}
broadcastFileStatus();
broadcastByteStatus();
}
private void onUploadThreadEnded() {
synchronized (this) {
Log.d(TAG, "UploadThread ended");
mNotificationManager.cancel(NOTIFY_ID_UPLOADING);
mUploadThread = null;
mUploading = false;
try {
mCallback.setUploading(false);
} catch (RemoteException e) {
}
}
stopServiceIfEmpty();
}
/**
* Callback from the UploadThread to the service.
*
* @param qf
* the queued file that was successfully uploaded.
*/
void onUploadComplete(QueuedFile qf) {
Log.d(TAG, "onUploadComplete of " + qf);
synchronized (this) {
if (!mFileBytesRemain.containsKey(qf)) {
Log.w(TAG, "onUploadComplete of un-queued file: " + qf);
return;
}
incrBytes(qf, qf.getSize());
mFileBytesRemain.remove(qf);
if (mFileBytesRemain.isEmpty()) {
// Fill up the percentage bars, since we could get
// this event before the periodic stats event.
// And at the end, we could kill camput between
// getting the final "file uploaded" event and the final
// stats event.
mFilesUploaded = mFilesTotal;
mBytesUploaded = mBytesTotal;
mNotificationManager.cancel(NOTIFY_ID_UPLOADING);
stopUploadThread();
}
mQueueList.remove(qf); // TODO: ghetto, linear scan
}
broadcastAllState();
stopServiceIfEmpty();
}
// incrBytes notes that size bytes of qf have been uploaded
// and updates mBytesUploaded.
private void incrBytes(QueuedFile qf, long size) {
synchronized (this) {
Long remain = mFileBytesRemain.get(qf);
if (remain != null) {
long actual = Math.min(size, remain.longValue());
mBytesUploaded += actual;
mFileBytesRemain.put(qf, remain - actual);
}
}
}
private void stopServiceIfEmpty() {
// Convenient place to drop this cache.
synchronized (this) {
if (mFileBytesRemain.isEmpty() && !mUploading && mUploadThread == null && !mPrefs.autoUpload()) {
Log.d(TAG, "stopServiceIfEmpty; stopping");
stopSelf();
} else {
Log.d(TAG, "stopServiceIfEmpty; NOT stopping; " + mFileBytesRemain.isEmpty() + "; " + mUploading + "; " + (mUploadThread != null));
return;
}
}
}
ParcelFileDescriptor getFileDescriptor(Uri uri) {
ContentResolver cr = getContentResolver();
try {
return cr.openFileDescriptor(uri, "r");
} catch (FileNotFoundException e) {
Log.w(TAG, "FileNotFound in getFileDescriptor() for " + uri);
return null;
}
}
private void incrementFilesToUpload(int size) throws RemoteException {
synchronized (UploadService.this) {
mFilesTotal += size;
}
broadcastFileStatus();
}
// pathOfURI tries to return the on-disk absolute path of uri.
// It may return null if it fails.
public String pathOfURI(Uri uri) {
if (uri == null) {
return null;
}
if ("file".equals(uri.getScheme())) {
return uri.getPath();
}
String[] proj = { MediaStore.Images.Media.DATA };
Cursor cursor = null;
try {
cursor = getContentResolver().query(uri, proj, null, null, null);
if (cursor == null) {
return null;
}
cursor.moveToFirst();
int columnIndex = cursor.getColumnIndex(proj[0]);
return cursor.getString(columnIndex); // might still be null
} finally {
if (cursor != null) {
cursor.close();
}
}
}
private final IUploadService.Stub service = new IUploadService.Stub() {
@Override
public int enqueueUploadList(List<Uri> uriList) throws RemoteException {
startService(new Intent(UploadService.this, UploadService.class));
Log.d(TAG, "enqueuing list of " + uriList.size() + " URIs");
incrementFilesToUpload(uriList.size());
int goodCount = 0;
for (Uri uri : uriList) {
goodCount += enqueueSingleUri(uri) ? 1 : 0;
}
Log.d(TAG, "...goodCount = " + goodCount);
return goodCount;
}
@Override
public boolean enqueueUpload(Uri uri) throws RemoteException {
startUploadService();
incrementFilesToUpload(1);
return enqueueSingleUri(uri);
}
private boolean enqueueSingleUri(Uri uri) throws RemoteException {
long statSize = 0;
{
ParcelFileDescriptor pfd = getFileDescriptor(uri);
if (pfd == null) {
incrementFilesToUpload(-1);
stopServiceIfEmpty();
return false;
}
try {
statSize = pfd.getStatSize();
} finally {
try {
pfd.close();
} catch (IOException e) {
}
}
}
String diskPath = pathOfURI(uri);
if (diskPath == null) {
Log.e(TAG, "failed to find pathOfURI(" + uri + ")");
return false;
}
Log.d(TAG, "diskPath of " + uri + " = " + diskPath);
QueuedFile qf = new QueuedFile(uri, statSize, diskPath);
boolean needResume = false;
synchronized (UploadService.this) {
if (mFileBytesRemain.containsKey(qf)) {
Log.d(TAG, "Dup blob enqueue, ignoring " + qf);
stopServiceIfEmpty();
return false;
}
Log.d(TAG, "Enqueueing blob: " + qf);
mFileBytesRemain.put(qf, qf.getSize());
mQueueList.add(qf);
if (mFileBytesRemain.size() == 1) {
mBytesTotal = 0;
mFilesTotal = 0;
mBytesUploaded = 0;
mFilesUploaded = 0;
}
mBytesTotal += qf.getSize();
mFilesTotal += 1;
needResume = !mUploading;
if (mUploadThread != null) {
mUploadThread.enqueueFile(qf);
}
}
broadcastFileStatus();
broadcastByteStatus();
if (needResume) {
resume();
}
return true;
}
@Override
public boolean isUploading() throws RemoteException {
synchronized (UploadService.this) {
return mUploading;
}
}
@Override
public void registerCallback(IStatusCallback cb) throws RemoteException {
// TODO: permit multiple listeners? when need comes.
synchronized (UploadService.this) {
if (cb == null) {
cb = DummyNullCallback.instance();
}
mCallback = cb;
}
broadcastAllState();
}
@Override
public void unregisterCallback(IStatusCallback cb) throws RemoteException {
synchronized (UploadService.this) {
mCallback = DummyNullCallback.instance();
}
}
@Override
public boolean resume() throws RemoteException {
Log.d(TAG, "Resuming upload...");
HostPort hp = mPrefs.hostPort();
if (!hp.isValid()) {
setUploadStatusText("Upload server not configured.");
return false;
}
final PowerManager.WakeLock wakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Camli Upload");
final WifiManager.WifiLock wifiLock = mWifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "Camli Upload");
synchronized (UploadService.this) {
if (mUploadThread != null) {
Log.d(TAG, "Already uploading; aborting resume.");
return false;
}
wakeLock.acquire();
wifiLock.acquire();
mNotificationBuilder = new Notification.Builder(UploadService.this);
mNotificationBuilder.setOngoing(true)
.setContentTitle("Uploading")
.setContentText("Camlistore uploader running")
.setSmallIcon(android.R.drawable.stat_sys_upload);
mNotificationManager.notify(NOTIFY_ID_UPLOADING, mNotificationBuilder.build());
mUploading = true;
mUploadThread = new UploadThread(UploadService.this, hp, mPrefs.trustedCert(), mPrefs.username(), mPrefs.password());
mUploadThread.start();
// Start a thread to release the wakelock...
final Thread threadToWatch = mUploadThread;
new Thread("UploadThread-waiter") {
@Override
public void run() {
while (true) {
try {
threadToWatch.join(10000); // 10 seconds
} catch (InterruptedException e) {
Log.d(TAG, "Interrupt waiting for uploader thread.", e);
}
synchronized (UploadService.this) {
if (threadToWatch.getState() == Thread.State.TERMINATED) {
break;
}
if (threadToWatch == mUploadThread) {
Log.d(TAG, "UploadThread-waiter still waiting.");
continue;
}
}
break;
}
Log.d(TAG, "UploadThread done; releasing the wakelock");
wakeLock.release();
wifiLock.release();
onUploadThreadEnded();
}
}.start();
}
mCallback.setUploading(true);
return true;
}
@Override
public boolean pause() throws RemoteException {
synchronized (UploadService.this) {
if (mUploadThread != null) {
stopUploadThread();
return true;
}
return false;
}
}
@Override
public int queueSize() throws RemoteException {
synchronized (UploadService.this) {
return mQueueList.size();
}
}
@Override
public void stopEverything() throws RemoteException {
synchronized (UploadService.this) {
mNotificationManager.cancel(NOTIFY_ID_UPLOADING);
mFileBytesRemain.clear();
mQueueList.clear();
mLastUploadStatusText = "Stopped";
mBytesInFlight = 0;
mFilesInFlight = 0;
mBytesTotal = 0;
mBytesUploaded = 0;
mFilesTotal = 0;
mFilesUploaded = 0;
stopUploadThread(); // recursive lock: okay
}
broadcastAllState();
}
@Override
public void setBackgroundWatchersEnabled(boolean enabled) throws RemoteException {
if (enabled) {
startUploadService();
UploadService.this.stopBackgroundWatchers();
UploadService.this.startBackgroundWatchers();
} else {
UploadService.this.stopBackgroundWatchers();
stopServiceIfEmpty();
}
}
};
public void onChunkUploaded(CamputChunkUploadedMessage msg) {
Log.d(TAG, "chunked uploaded for " + msg.queuedFile() + " with size " + msg.size());
synchronized (UploadService.this) {
incrBytes(msg.queuedFile(), msg.size());
}
broadcastAllState();
}
public void onStatReceived(String stat, long value) {
String v;
synchronized (UploadService.this) {
if (stat == null) {
mStatValue.clear();
} else {
mStatValue.put(stat, value);
}
StringBuilder sb = new StringBuilder();
for (Entry<String, Long> ent : mStatValue.entrySet()) {
sb.append(ent.getKey());
sb.append(": ");
sb.append(ent.getValue());
sb.append("\n");
}
v = sb.toString();
mLastUploadStatsText = v;
}
try {
mCallback.setUploadStatsText(v);
} catch (RemoteException e) {
}
}
protected void stopUploadThread() {
synchronized (UploadService.this) {
mNotificationManager.cancel(NOTIFY_ID_UPLOADING);
if (mUploadThread != null) {
mUploadThread.stopUploads();
mUploadThread = null;
try {
mCallback.setUploading(false);
} catch (RemoteException e) {
}
}
mUploading = false;
}
}
public void onStatsReceived(CamputStatsMessage msg) {
synchronized (UploadService.this) {
mBytesTotal = msg.totalBytes();
mFilesTotal = (int) msg.totalFiles();
mBytesUploaded = msg.skippedBytes() + msg.uploadedBytes();
mFilesUploaded = (int) (msg.skippedFiles() + msg.uploadedFiles());
}
broadcastAllState();
}
}

View File

@@ -0,0 +1,409 @@
/*
Copyright 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.camlistore;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.HashMap;
import java.util.ListIterator;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.util.Log;
public class UploadThread extends Thread {
private static final String TAG = "UploadThread";
private final UploadService mService;
private final HostPort mHostPort;
private final String mTrustedCert;
private final String mUsername;
private final String mPassword;
private final LinkedBlockingQueue<UploadThreadMessage> msgCh = new LinkedBlockingQueue<UploadThreadMessage>();
AtomicReference<Process> goProcess = new AtomicReference<Process>();
AtomicReference<OutputStream> toChildRef = new AtomicReference<OutputStream>();
HashMap<String, QueuedFile> mQueuedFile = new HashMap<String, QueuedFile>(); // guarded
// by
// itself
private final Object stdinLock = new Object(); // guards setting and writing
// to stdinWriter
private BufferedWriter stdinWriter;
public UploadThread(UploadService uploadService, HostPort hp, String trustedCert, String username, String password) {
mService = uploadService;
mHostPort = hp;
mTrustedCert = trustedCert != null ? trustedCert.toLowerCase().trim() : "";
mUsername = username;
mPassword = password;
}
public void stopUploads() {
Process p = goProcess.get();
if (p == null) {
return;
}
synchronized (stdinLock) {
if (stdinWriter == null) {
// force kill. confused.
p.destroy();
goProcess.set(null);
return;
}
try {
stdinWriter.close();
Log.d(TAG, "Closed camput's stdin");
stdinWriter = null;
} catch (IOException e) {
p.destroy(); // force kill
goProcess.set(null);
return;
}
// Unnecessary paranoia, never seen in practice:
new Thread() {
@Override
public void run() {
try {
Thread.sleep(750, 0);
stopUploads(); // force kill if still alive.
} catch (InterruptedException e) {
}
}
}.start();
}
}
private String binaryPath(String suffix) {
return mService.getBaseContext().getFilesDir().getAbsolutePath() + "/" + suffix;
}
private void status(String st) {
Log.d(TAG, st);
mService.setUploadStatusText(st);
}
// An UploadThreadMessage can be sent on msgCh and read by the run() method
// in
// until it's time to quit the thread.
private static class UploadThreadMessage {
}
private static class ProcessExitedMessage extends UploadThreadMessage {
public int code;
public ProcessExitedMessage(int code) {
this.code = code;
}
}
public boolean enqueueFile(QueuedFile qf) {
String diskPath = qf.getDiskPath();
if (diskPath == null) {
Log.d(TAG, "file has no disk path: " + qf);
return false;
}
synchronized (stdinLock) {
if (stdinWriter == null) {
return false;
}
synchronized (mQueuedFile) {
mQueuedFile.put(diskPath, qf);
}
try {
stdinWriter.write(diskPath + "\n");
stdinWriter.flush();
} catch (IOException e) {
Log.d(TAG, "Failed to write " + diskPath + " to camput stdin: " + e);
return false;
}
}
return true;
}
@Override
public void run() {
Log.d(TAG, "Running");
if (!mHostPort.isValid()) {
Log.d(TAG, "host/port is invalid");
return;
}
status("Running UploadThread for " + mHostPort);
mService.onStatReceived(null, 0);
Process process = null;
try {
ProcessBuilder pb = new ProcessBuilder();
pb.command(binaryPath("camput.bin"), "--server=" + mHostPort.urlPrefix(), "file", "-stdinargs", "-vivify");
pb.redirectErrorStream(false);
pb.environment().put("CAMLI_AUTH", "userpass:" + mUsername + ":" + mPassword);
pb.environment().put("CAMLI_TRUSTED_CERT", mTrustedCert);
pb.environment().put("CAMLI_CACHE_DIR", mService.getCacheDir().getAbsolutePath());
pb.environment().put("CAMPUT_ANDROID_OUTPUT", "1");
process = pb.start();
goProcess.set(process);
synchronized (stdinLock) {
stdinWriter = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), "UTF-8"));
}
new CopyToAndroidLogThread("stderr", process.getErrorStream()).start();
new ParseCamputOutputThread(process, mService).start();
new WaitForProcessThread(process).start();
} catch (IOException e) {
throw new RuntimeException(e);
}
ListIterator<QueuedFile> iter = mService.uploadQueue().listIterator();
while (iter.hasNext()) {
enqueueFile(iter.next());
}
// Loop forever reading from msgCh
while (true) {
UploadThreadMessage msg = null;
try {
msg = msgCh.poll(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
continue;
}
if (msg instanceof ProcessExitedMessage) {
status("Upload process ended.");
ProcessExitedMessage pem = (ProcessExitedMessage) msg;
Log.d(TAG, "Loop exiting; code was = " + pem.code);
return;
}
}
}
// "CHUNK_UPLOADED %d %s %s\n", sb.Size, blob, asr.path
private final static Pattern chunkUploadedPattern = Pattern.compile("^CHUNK_UPLOADED (\\d+) (\\S+) (.+)");
public class CamputChunkUploadedMessage {
private final String mFilename;
private final long mSize;
public CamputChunkUploadedMessage(String line) {
Matcher m = chunkUploadedPattern.matcher(line);
if (!m.matches()) {
throw new RuntimeException("bogus CamputChunkMessage: " + line);
}
mSize = Long.parseLong(m.group(1));
mFilename = m.group(3);
}
public QueuedFile queuedFile() {
synchronized (mQueuedFile) {
return mQueuedFile.get(mFilename);
}
}
public long size() {
return mSize;
}
}
// STAT %s %d\n
private final static Pattern statPattern = Pattern.compile("^STAT (\\S+) (\\d+)\\b");
public class CamputStatMessage {
private final Matcher mm;
public CamputStatMessage(String line) {
mm = statPattern.matcher(line);
if (!mm.matches()) {
throw new RuntimeException("bogus CamputStatMessage: " + line);
}
}
public String name() {
return mm.group(1);
}
public long value() {
return Long.parseLong(mm.group(2));
}
}
// STATS nfile=%d nbyte=%d skfile=%d skbyte=%d upfile=%d upbyte=%d\n
private final static Pattern statsPattern = Pattern.compile("^STATS nfile=(\\d+) nbyte=(\\d+) skfile=(\\d+) skbyte=(\\d+) upfile=(\\d+) upbyte=(\\d+)");
public class CamputStatsMessage {
private final Matcher mm;
public CamputStatsMessage(String line) {
mm = statsPattern.matcher(line);
if (!mm.matches()) {
throw new RuntimeException("bogus CamputStatsMessage: " + line);
}
}
private long field(int n) {
return Long.parseLong(mm.group(n));
}
public long totalFiles() {
return field(1);
}
public long totalBytes() {
return field(2);
}
public long skippedFiles() {
return field(3);
}
public long skippedBytes() {
return field(4);
}
public long uploadedFiles() {
return field(5);
}
public long uploadedBytes() {
return field(6);
}
}
private class ParseCamputOutputThread extends Thread {
private final BufferedReader mBufIn;
private final UploadService mService;
private final static String TAG = UploadThread.TAG + "/camput-out";
private final static boolean DEBUG_CAMPUT_ACTIVITY = false;
public ParseCamputOutputThread(Process process, UploadService service) {
mService = service;
mBufIn = new BufferedReader(new InputStreamReader(process.getInputStream()));
}
@Override
public void run() {
while (true) {
String line = null;
try {
line = mBufIn.readLine();
} catch (IOException e) {
Log.d(TAG, "Exception reading camput's stdout: " + e.toString());
return;
}
if (DEBUG_CAMPUT_ACTIVITY) {
Log.d(TAG, "camput: " + line);
}
if (line == null) {
// EOF
return;
}
if (line.startsWith("CHUNK_UPLOADED ")) {
CamputChunkUploadedMessage msg = new CamputChunkUploadedMessage(line);
mService.onChunkUploaded(msg);
continue;
}
if (line.startsWith("STATS ")) {
CamputStatsMessage msg = new CamputStatsMessage(line);
mService.onStatsReceived(msg);
continue;
}
if (line.startsWith("STAT ")) {
CamputStatMessage msg = new CamputStatMessage(line);
mService.onStatReceived(msg.name(), msg.value());
continue;
}
if (line.startsWith("FILE_UPLOADED ")) {
String filename = line.substring(14).trim();
QueuedFile qf = null;
synchronized (mQueuedFile) {
qf = mQueuedFile.get(filename);
if (qf != null) {
mQueuedFile.remove(filename);
}
}
if (qf != null) {
mService.onUploadComplete(qf);
}
continue;
}
Log.d(TAG, "camput said unknown line: " + line);
}
}
}
private class WaitForProcessThread extends Thread {
private final Process mProcess;
public WaitForProcessThread(Process p) {
mProcess = p;
}
@Override
public void run() {
Log.d(TAG, "Waiting for camput process.");
try {
mProcess.waitFor();
} catch (InterruptedException e) {
Log.d(TAG, "Interrupted waiting for camput");
msgCh.offer(new ProcessExitedMessage(-1));
return;
}
Log.d(TAG, "Exit status of camput = " + mProcess.exitValue());
msgCh.offer(new ProcessExitedMessage(mProcess.exitValue()));
}
}
// CopyToAndroidLogThread copies the camput child process's stderr
// to Android's log.
private static class CopyToAndroidLogThread extends Thread {
private final BufferedReader mBufIn;
private final String mStream;
public CopyToAndroidLogThread(String stream, InputStream in) {
mBufIn = new BufferedReader(new InputStreamReader(in));
mStream = stream;
}
@Override
public void run() {
String tag = TAG + "/" + mStream + "-child";
while (true) {
String line = null;
try {
line = mBufIn.readLine();
} catch (IOException e) {
Log.d(tag, "Exception: " + e.toString());
return;
}
if (line == null) {
// EOF
return;
}
Log.d(tag, line);
}
}
}
}

View File

@@ -0,0 +1,149 @@
/*
Copyright 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.camlistore;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.locks.ReentrantLock;
import android.os.AsyncTask;
import android.os.Looper;
import android.util.Base64;
import android.util.Log;
public class Util {
private static final String TAG = "Camli_Util";
public static String slurp(InputStream in) throws IOException {
StringBuilder sb = new StringBuilder();
byte[] b = new byte[4096];
for (int n; (n = in.read(b)) != -1;) {
sb.append(new String(b, 0, n));
}
return sb.toString();
}
public static byte[] slurpToByteArray(InputStream inputStream) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
for (int numRead; (numRead = inputStream.read(buffer)) != -1;) {
outputStream.write(buffer, 0, numRead);
}
return outputStream.toByteArray();
}
public static void copyFile(File fromFile, File toFile) throws IOException {
FileInputStream inputStream = new FileInputStream(fromFile);
FileOutputStream outputStream = new FileOutputStream(toFile);
byte[] buffer = new byte[4096];
for (int numRead; (numRead = inputStream.read(buffer)) != -1;)
outputStream.write(buffer, 0, numRead);
inputStream.close();
outputStream.close();
}
public static void runAsync(final Runnable r) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... unused) {
r.run();
return null;
}
}.execute();
}
public static boolean onMainThread() {
return Looper.myLooper() == Looper.getMainLooper();
}
public static void assertMainThread() {
if (!onMainThread()) {
throw new RuntimeException("Assert: unexpected call off the main thread");
}
}
public static void assertNotMainThread() {
if (onMainThread()) {
throw new RuntimeException("Assert: unexpected call on main thread");
}
}
// Asserts that |lock| is held by the current thread.
public static void assertLockIsHeld(ReentrantLock lock) {
if (!lock.isHeldByCurrentThread()) {
throw new RuntimeException("Assert: mandatory lock isn't held by current thread");
}
}
// Asserts that |lock| is not held by the current thread.
public static void assertLockIsNotHeld(ReentrantLock lock) {
if (lock.isHeldByCurrentThread()) {
throw new RuntimeException("Assert: lock is held by current thread but shouldn't be");
}
}
private static final String HEX = "0123456789abcdef";
public static String getHex(byte[] raw) {
if (raw == null) {
return null;
}
final StringBuilder hex = new StringBuilder(2 * raw.length);
for (final byte b : raw) {
hex.append(HEX.charAt((b & 0xF0) >> 4)).append(
HEX.charAt((b & 0x0F)));
}
return hex.toString();
}
// Requires that the fd be seeked to the beginning.
public static String getSha1(FileDescriptor fd) {
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
byte[] b = new byte[4096];
FileInputStream fis = new FileInputStream(fd);
InputStream is = new BufferedInputStream(fis, 4096);
try {
for (int n; (n = is.read(b)) != -1;) {
md.update(b, 0, n);
}
} catch (IOException e) {
Log.w(TAG, "IOException while computing SHA-1");
return null;
}
byte[] sha1hash = new byte[40];
sha1hash = md.digest();
return getHex(sha1hash);
}
public static String getBasicAuthHeaderValue(String username, String password) {
return "Basic " + Base64.encodeToString((username + ":" + password).getBytes(),
Base64.NO_WRAP);
}
}

View File

@@ -0,0 +1,90 @@
/*
Copyright 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.camlistore;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.util.Log;
public class WifiPowerReceiver extends BroadcastReceiver {
private static final String TAG = "WifiPowerReceiver";
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Log.d(TAG, "intent: " + intent);
if (Intent.ACTION_POWER_CONNECTED.equals(action)) {
Intent cmd = new Intent(UploadService.INTENT_POWER_CONNECTED);
cmd.setClass(context, UploadService.class);
context.startService(cmd);
return;
}
if (Intent.ACTION_POWER_DISCONNECTED.equals(action)) {
Intent cmd = new Intent(UploadService.INTENT_POWER_DISCONNECTED);
cmd.setClass(context, UploadService.class);
context.startService(cmd);
}
if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) {
boolean wifi = onWifi(context);
Log.d(TAG, "onWifi = " + wifi);
Intent cmd = new Intent(wifi ? UploadService.INTENT_NETWORK_WIFI : UploadService.INTENT_NETWORK_NOT_WIFI);
String ssid = getSSID(context);
cmd.putExtra("SSID", ssid);
Log.d(TAG, "extra ssid (chk)= " + cmd.getStringExtra("SSID"));
cmd.setClass(context, UploadService.class);
context.startService(cmd);
}
}
public static boolean onPower(Context context) {
// TODO Auto-generated method stub
return false;
}
public static boolean onWifi(Context context) {
NetworkInfo ni = ((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
if (ni != null && ni.isConnected()
&& (ni.getType() == ConnectivityManager.TYPE_WIFI || ni.getType() == ConnectivityManager.TYPE_ETHERNET)) {
return true;
}
return false;
}
public static String getSSID(Context context) {
NetworkInfo ni = ((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
if (ni != null && ni.isConnected()
&& (ni.getType() == ConnectivityManager.TYPE_WIFI || ni.getType() == ConnectivityManager.TYPE_ETHERNET)) {
WifiManager wifiMgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
if (wifiMgr != null) {
WifiInfo wifiInfo = wifiMgr.getConnectionInfo();
String ssid = wifiInfo.getSSID();
if (ssid.startsWith("\"") && ssid.endsWith("\"")){
ssid = ssid.substring(1, ssid.length()-1);
}
return ssid;
}
}
return "";
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry combineaccessrules="false" kind="src" path="/camlistore"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>camlistoreTest</name>
<comment></comment>
<projects>
<project>camlistore</project>
</projects>
<buildSpec>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,4 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
org.eclipse.jdt.core.compiler.compliance=1.6
org.eclipse.jdt.core.compiler.source=1.6

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.camlistore.test"
android:versionCode="1"
android:versionName="1.0">
<application android:icon="@drawable/icon" android:label="@string/app_name">
<uses-library android:name="android.test.runner" />
</application>
<instrumentation android:targetPackage="org.camlistore" android:name="android.test.InstrumentationTestRunner" />
</manifest>

View File

@@ -0,0 +1,21 @@
# This file is used to override default values used by the Ant build system.
#
# This file must be checked in Version Control Systems, as it is
# integral to the build system of your project.
# This file is only used by the Ant script.
# You can use this to override default values such as
# 'source.dir' for the location of your java source folder and
# 'out.dir' for the location of your output folder.
# You can also use it define how the release builds are signed by declaring
# the following properties:
# 'key.store' for the location of your keystore and
# 'key.alias' for the name of the key to use.
# The password will be asked during the build when you use the 'release' target.
tested.project.dir=..
out.dir=build
gen.dir=build/gen

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="CamliActivityTest" default="help">
<!-- The local.properties file is created and updated by the 'android' tool.
It contains the path to the SDK. It should *NOT* be checked in in Version
Control Systems. -->
<property file="local.properties" />
<!-- The build.properties file can be created by you and is never touched
by the 'android' tool. This is the place to change some of the default property values
used by the Ant rules.
Here are some properties you may want to change/update:
application.package
the name of your application package as defined in the manifest. Used by the
'uninstall' rule.
source.dir
the name of the source directory. Default is 'src'.
out.dir
the name of the output directory. Default is 'bin'.
Properties related to the SDK location or the project target should be updated
using the 'android' tool with the 'update' action.
This file is an integral part of the build system for your application and
should be checked in in Version Control Systems.
-->
<property file="build.properties" />
<!-- The default.properties file is created and updated by the 'android' tool, as well
as ADT.
This file is an integral part of the build system for your application and
should be checked in in Version Control Systems. -->
<property file="default.properties" />
<!-- Custom Android task to deal with the project target, and import the proper rules.
This requires ant 1.6.0 or above. -->
<path id="android.antlibs">
<pathelement path="${sdk.dir}/tools/lib/anttasks.jar" />
<pathelement path="${sdk.dir}/tools/lib/sdklib.jar" />
<pathelement path="${sdk.dir}/tools/lib/androidprefs.jar" />
<pathelement path="${sdk.dir}/tools/lib/apkbuilder.jar" />
<pathelement path="${sdk.dir}/tools/lib/jarutils.jar" />
</path>
<taskdef name="setup"
classname="com.android.ant.SetupTask"
classpathref="android.antlibs" />
<!-- Execute the Android Setup task that will setup some properties specific to the target,
and import the build rules files.
The rules file is imported from
<SDK>/platforms/<target_platform>/templates/android_rules.xml
To customize some build steps for your project:
- copy the content of the main node <project> from android_rules.xml
- paste it in this build.xml below the <setup /> task.
- disable the import by changing the setup task below to <setup import="false" />
This will ensure that the properties are setup correctly but that your customized
build steps are used.
-->
<setup />
<!-- See:
http://www.alittlemadness.com/2010/05/31/setting-up-an-android-project-build/
and:
http://code.google.com/p/android/issues/detail?id=8001
-->
<property name="extensible.classpath" value="${tested.project.absolute.dir}/${out.dir}/classes"/>
</project>

View File

@@ -0,0 +1,11 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system use,
# "build.properties", and override values to adapt the script to your
# project structure.
# Project target.
target=android-4

View File

@@ -0,0 +1,14 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-10

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/hello"
/>
</LinearLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="hello">Hello World!</string>
<string name="app_name">camlistoreTest</string>
</resources>

View File

@@ -0,0 +1,32 @@
/*
Copyright 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.camlistore;
import android.test.ActivityInstrumentationTestCase2;
public class CamliActivityTest extends ActivityInstrumentationTestCase2<CamliActivity> {
public CamliActivityTest(String pkg, Class<CamliActivity> activityClass) {
super(pkg, activityClass);
// TODO Auto-generated constructor stub
}
public void testSanity() {
assertEquals(2, 1 + 1);
assertEquals(4, 2 + 2);
}
}

View File

@@ -0,0 +1,183 @@
// From http://code.google.com/p/crypto-js/
// License: http://www.opensource.org/licenses/bsd-license.php
//
// Copyright (c) 2009, Jeff Mott. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer. Redistributions in binary
// form must reproduce the above copyright notice, this list of conditions and
// the following disclaimer in the documentation and/or other materials provided
// with the distribution. Neither the name Crypto-JS nor the names of its
// contributors may be used to endorse or promote products derived from this
// software without specific prior written permission. THIS SOFTWARE IS PROVIDED
// BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
// EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
if (typeof Crypto == "undefined" || ! Crypto.util)
{
(function(){
var base64map = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
// Global Crypto object
var Crypto = window.Crypto = {};
// Crypto utilities
var util = Crypto.util = {
// Bit-wise rotate left
rotl: function (n, b) {
return (n << b) | (n >>> (32 - b));
},
// Bit-wise rotate right
rotr: function (n, b) {
return (n << (32 - b)) | (n >>> b);
},
// Swap big-endian to little-endian and vice versa
endian: function (n) {
// If number given, swap endian
if (n.constructor == Number) {
return util.rotl(n, 8) & 0x00FF00FF |
util.rotl(n, 24) & 0xFF00FF00;
}
// Else, assume array and swap all items
for (var i = 0; i < n.length; i++)
n[i] = util.endian(n[i]);
return n;
},
// Generate an array of any length of random bytes
randomBytes: function (n) {
for (var bytes = []; n > 0; n--)
bytes.push(Math.floor(Math.random() * 256));
return bytes;
},
// Convert a byte array to big-endian 32-bit words
bytesToWords: function (bytes) {
for (var words = [], i = 0, b = 0; i < bytes.length; i++, b += 8)
words[b >>> 5] |= bytes[i] << (24 - b % 32);
return words;
},
// Convert big-endian 32-bit words to a byte array
wordsToBytes: function (words) {
for (var bytes = [], b = 0; b < words.length * 32; b += 8)
bytes.push((words[b >>> 5] >>> (24 - b % 32)) & 0xFF);
return bytes;
},
// Convert a byte array to a hex string
bytesToHex: function (bytes) {
for (var hex = [], i = 0; i < bytes.length; i++) {
hex.push((bytes[i] >>> 4).toString(16));
hex.push((bytes[i] & 0xF).toString(16));
}
return hex.join("");
},
// Convert a hex string to a byte array
hexToBytes: function (hex) {
for (var bytes = [], c = 0; c < hex.length; c += 2)
bytes.push(parseInt(hex.substr(c, 2), 16));
return bytes;
},
// Convert a byte array to a base-64 string
bytesToBase64: function (bytes) {
// Use browser-native function if it exists
if (typeof btoa == "function") return btoa(Binary.bytesToString(bytes));
for(var base64 = [], i = 0; i < bytes.length; i += 3) {
var triplet = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
for (var j = 0; j < 4; j++) {
if (i * 8 + j * 6 <= bytes.length * 8)
base64.push(base64map.charAt((triplet >>> 6 * (3 - j)) & 0x3F));
else base64.push("=");
}
}
return base64.join("");
},
// Convert a base-64 string to a byte array
base64ToBytes: function (base64) {
// Use browser-native function if it exists
if (typeof atob == "function") return Binary.stringToBytes(atob(base64));
// Remove non-base-64 characters
base64 = base64.replace(/[^A-Z0-9+\/]/ig, "");
for (var bytes = [], i = 0, imod4 = 0; i < base64.length; imod4 = ++i % 4) {
if (imod4 == 0) continue;
bytes.push(((base64map.indexOf(base64.charAt(i - 1)) & (Math.pow(2, -2 * imod4 + 8) - 1)) << (imod4 * 2)) |
(base64map.indexOf(base64.charAt(i)) >>> (6 - imod4 * 2)));
}
return bytes;
}
};
// Crypto mode namespace
Crypto.mode = {};
// Crypto character encodings
var charenc = Crypto.charenc = {};
// UTF-8 encoding
var UTF8 = charenc.UTF8 = {
// Convert a string to a byte array
stringToBytes: function (str) {
return Binary.stringToBytes(unescape(encodeURIComponent(str)));
},
// Convert a byte array to a string
bytesToString: function (bytes) {
return decodeURIComponent(escape(Binary.bytesToString(bytes)));
}
};
// Binary encoding
var Binary = charenc.Binary = {
// Convert a string to a byte array
stringToBytes: function (str) {
for (var bytes = [], i = 0; i < str.length; i++)
bytes.push(str.charCodeAt(i));
return bytes;
},
// Convert a byte array to a string
bytesToString: function (bytes) {
for (var str = [], i = 0; i < bytes.length; i++)
str.push(String.fromCharCode(bytes[i]));
return str.join("");
}
};
})();
}

View File

@@ -0,0 +1,13 @@
Copyright 2010 Brett Slatkin
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,109 @@
// From http://code.google.com/p/crypto-js/
// License: http://www.opensource.org/licenses/bsd-license.php
//
// Copyright (c) 2009, Jeff Mott. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer. Redistributions in binary
// form must reproduce the above copyright notice, this list of conditions and
// the following disclaimer in the documentation and/or other materials provided
// with the distribution. Neither the name Crypto-JS nor the names of its
// contributors may be used to endorse or promote products derived from this
// software without specific prior written permission. THIS SOFTWARE IS PROVIDED
// BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
// EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
(function(){
// Shortcuts
var C = Crypto,
util = C.util,
charenc = C.charenc,
UTF8 = charenc.UTF8,
Binary = charenc.Binary;
// Public API
var SHA1 = C.SHA1 = function (message, options) {
var digestbytes = util.wordsToBytes(SHA1._sha1(message));
return options && options.asBytes ? digestbytes :
options && options.asString ? Binary.bytesToString(digestbytes) :
util.bytesToHex(digestbytes);
};
// The core
SHA1._sha1 = function (message) {
// Convert to byte array
if (message.constructor == String) message = UTF8.stringToBytes(message);
/* else, assume byte array already */
var m = util.bytesToWords(message),
l = message.length * 8,
w = [],
H0 = 1732584193,
H1 = -271733879,
H2 = -1732584194,
H3 = 271733878,
H4 = -1009589776;
// Padding
m[l >> 5] |= 0x80 << (24 - l % 32);
m[((l + 64 >>> 9) << 4) + 15] = l;
for (var i = 0; i < m.length; i += 16) {
var a = H0,
b = H1,
c = H2,
d = H3,
e = H4;
for (var j = 0; j < 80; j++) {
if (j < 16) w[j] = m[i + j];
else {
var n = w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16];
w[j] = (n << 1) | (n >>> 31);
}
var t = ((H0 << 5) | (H0 >>> 27)) + H4 + (w[j] >>> 0) + (
j < 20 ? (H1 & H2 | ~H1 & H3) + 1518500249 :
j < 40 ? (H1 ^ H2 ^ H3) + 1859775393 :
j < 60 ? (H1 & H2 | H1 & H3 | H2 & H3) - 1894007588 :
(H1 ^ H2 ^ H3) - 899497514);
H4 = H3;
H3 = H2;
H2 = (H1 << 30) | (H1 >>> 2);
H1 = H0;
H0 = t;
}
H0 += a;
H1 += b;
H2 += c;
H3 += d;
H4 += e;
}
return [H0, H1, H2, H3, H4];
};
// Package private blocksize
SHA1._blocksize = 16;
})();

View File

@@ -0,0 +1,256 @@
<html>
<head>
<script type="text/javascript" src="base64.js"></script>
<script type="text/javascript" src="jquery-1.4.2.min.js"></script>
<script type="text/javascript" src="chrome_ex_oauthsimple.js"></script>
<script type="text/javascript" src="chrome_ex_oauth.js"></script>
<script type="text/javascript" src="Crypto.js"></script>
<script type="text/javascript" src="SHA1.js"></script>
<script type="text/javascript" charset="utf-8">
var OAUTH = ChromeExOAuth.initBackgroundPage({
'request_url' : 'https://www.google.com/accounts/OAuthGetRequestToken',
'authorize_url' : 'https://www.google.com/accounts/OAuthAuthorizeToken',
'access_url' : 'https://www.google.com/accounts/OAuthGetAccessToken',
'consumer_key' : 'anonymous',
'consumer_secret' : 'anonymous',
'scope' : 'http://picasaweb.google.com/data/',
'app_name' : 'Clip It Good: Chrome Extension'
});
// Constants for various album types.
var PICASA = 'picasa';
var CAMLISTORE = 'camlistore';
var ALBUM_TYPE_STRING = {
picasa: 'Picasa Web Albums',
camlistore: 'Camlistore'
};
// Preferences
var ALBUM_CONFIG = {}; // 'type' -> ('id' -> 'name')
var ALBUM_OPTIONS = {}; // 'type' -> ('id' -> optionsDict)
function loadAlbumConfig() {
var newAlbumConfig = localStorage.getItem('config:albums');
if (newAlbumConfig) {
ALBUM_CONFIG = $.parseJSON(newAlbumConfig);
}
var newAlbumOptions = localStorage.getItem('config:albumOptions');
if (newAlbumOptions) {
ALBUM_OPTIONS = $.parseJSON(newAlbumOptions);
}
}
function saveAlbumConfig() {
localStorage.setItem('config:albums', JSON.stringify(ALBUM_CONFIG));
localStorage.setItem('config:albumOptions', JSON.stringify(ALBUM_OPTIONS));
}
// Sort albums by name.
function getSortedAlbums(albumIdNameDict) {
var albumIdNameArray = [];
$.each(albumIdNameDict, function(id, name) {
albumIdNameArray.push({'id': id, 'name': name});
});
albumIdNameArray.sort(function(a, b) {
if (a['name'] < b['name']) {
return -1;
} else if (a['name'] > b['name']) {
return 1;
} else {
return 0;
}
});
return albumIdNameArray;
}
function setupMenus() {
loadAlbumConfig();
chrome.contextMenus.removeAll(function() {
$.each(ALBUM_CONFIG, function(albumType, albumIdNameDict) {
chrome.contextMenus.create({
title: ALBUM_TYPE_STRING[albumType],
contexts: ['image']
});
chrome.contextMenus.create({
type: 'separator',
contexts: ['image']
});
$.each(getSortedAlbums(albumIdNameDict), function(index, albumDict) {
chrome.contextMenus.create({
title: albumDict.name,
contexts: ['image'],
onclick: function(data, tab) {
return handleMenuClick(
albumType, albumDict.name, albumDict.id, data, tab);
}
});
});
});
});
}
// Upload actions
var ALBUM_TYPE_UPLOAD_FUNC = {
picasa: function(albumId, albumName, dataSrcUrl, dataBuffer, uploadDone) {
var builder = new BlobBuilder();
builder.append(dataBuffer);
function complete(resp, xhr) {
if (!(xhr.status >= 200 && xhr.status <= 299)) {
alert('Error: Response status = ' + xhr.status +
', response body = "' + xhr.responseText + '"');
} else {
uploadDone();
}
}
OAUTH.authorize(function() {
OAUTH.sendSignedRequest(
'http://picasaweb.google.com/data/feed/api/' +
'user/default/albumid/' + albumId,
complete,
{
method: 'POST',
headers: {
'Content-Type': 'image/png',
'Slug': dataSrcUrl
},
parameters: {
alt: 'json'
},
body: builder.getBlob('image/png')
}
);
});
},
camlistore: function(albumId, albumName, dataSrcUrl,
dataBuffer, uploadDone) {
var hash = Crypto.SHA1(new Uint8Array(dataBuffer, 0));
var blobRef = 'sha1-' + hash;
var options = ALBUM_OPTIONS[CAMLISTORE][albumId];
function doUpload(uploadUrl) {
// XXX Use real random boundary.
var boundary = 'randomboundaryXYZ';
var contentType = 'multipart/form-data; boundary=' + boundary;
var header =
'--' + boundary + '\r\n' +
'Content-Type: application/octet-stream\r\n' +
'Content-Disposition: form-data; name="' + blobRef +
'"; filename="1"\r\n\r\n'
var footer = '\r\n--' + boundary + '--\r\n';
var builder = new BlobBuilder();
builder.append(header);
builder.append(dataBuffer);
builder.append(footer);
var payload = builder.getBlob(contentType);
var uploadXhr = new XMLHttpRequest();
uploadXhr.open('POST', uploadUrl, true,
options.username, options.password);
uploadXhr.onreadystatechange = function() {
if (uploadXhr.readyState == XMLHttpRequest.DONE &&
uploadXhr.status == 200) {
// XXX Check for bad response format (not JSON).
var responseJson = $.parseJSON(uploadXhr.responseText)
if (responseJson.received &&
responseJson.received.length == 1 &&
responseJson.received[0].blobRef == blobRef) {
console.log('Successful upload: ' + blobRef);
uploadDone();
return;
}
alert('Camlistore upload response did not verify blob "' +
blobRef + '": ' + uploadXhr.responseText);
}
// XXX: Handle request errors
}
uploadXhr.setRequestHeader('Content-Type', contentType);
uploadXhr.send(payload);
}
var statXhr = new XMLHttpRequest();
statXhr.open('POST', albumId + '/camli/stat', true,
options.username, options.password);
statXhr.onreadystatechange = function() {
if (statXhr.readyState == XMLHttpRequest.DONE &&
statXhr.status == 200) {
// XXX Check for bad response format (not JSON).
var responseJson = $.parseJSON(statXhr.responseText);
if (responseJson.stat &&
responseJson.stat.length == 1 &&
responseJson.stat[0].blobRef == blobRef) {
console.log('Blob already present: ' + blobRef);
uploadDone();
return;
}
var uploadUrl = responseJson.uploadUrl;
if (!uploadUrl) {
alert('Camlistore stat response missing "uploadUrl": ' +
statXhr.responseText);
return;
}
doUpload(uploadUrl);
}
// XXX: Handle request errors
}
statXhr.setRequestHeader(
'Content-Type', 'application/x-www-form-urlencoded');
statXhr.send('camliversion=1&blob1=' + blobRef);
}
};
function handleMenuClick(albumType, albumName, albumId, data, tab) {
chrome.pageAction.setTitle({
tabId: tab.id,
title: 'Clip It Good: Uploading (' + data.srcUrl.substr(0, 100) + ')'
});
chrome.pageAction.show(tab.id);
var img = document.createElement('img');
img.onload = function() {
var canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext('2d').drawImage(img, 0, 0);
var dataUrl = canvas.toDataURL();
var dataUrlAdjusted = dataUrl.replace('data:image/png;base64,', '');
var arrayBuffer = Base64.decode(dataUrlAdjusted).buffer;
function uploadDone() {
chrome.pageAction.hide(tab.id);
}
var uploadFunc = ALBUM_TYPE_UPLOAD_FUNC[albumType];
uploadFunc(albumId, albumName, data.srcUrl, arrayBuffer, uploadDone);
} // end onload
img.src = data.srcUrl;
}
$(document).ready( function() {
setupMenus();
});
</script>
</head>
<body>
</body>
</html>

View File

@@ -0,0 +1,206 @@
/*
Copyright (c) 2008 Fred Palmer fred.palmer_at_gmail.com
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/
function StringBuffer()
{
this.buffer = [];
}
StringBuffer.prototype.append = function append(string)
{
this.buffer.push(string);
return this;
};
StringBuffer.prototype.toString = function toString()
{
return this.buffer.join("");
};
var Base64 =
{
codex : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
encode : function (input)
{
var output = new StringBuffer();
var enumerator = new Utf8EncodeEnumerator(input);
while (enumerator.moveNext())
{
var chr1 = enumerator.current;
enumerator.moveNext();
var chr2 = enumerator.current;
enumerator.moveNext();
var chr3 = enumerator.current;
var enc1 = chr1 >> 2;
var enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
var enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
var enc4 = chr3 & 63;
if (isNaN(chr2))
{
enc3 = enc4 = 64;
}
else if (isNaN(chr3))
{
enc4 = 64;
}
output.append(this.codex.charAt(enc1) + this.codex.charAt(enc2) + this.codex.charAt(enc3) + this.codex.charAt(enc4));
}
return output.toString();
},
decode : function (input)
{
// TypedArray usage added by brett@haxor.com 11/27/2010
var size = 0;
var buffer = new ArrayBuffer(input.length);
var output = new Uint8Array(buffer, 0);
var enumerator = new Base64DecodeEnumerator(input);
while (enumerator.moveNext()) {
output[size++] = enumerator.current;
}
// There is nothing in the TypedArray spec to copy/subset a buffer,
// so we have to do a copy to ensure that typedarray.buffer is the
// correct length when passed to XmlHttpRequest methods, etc.
var outputBuffer = new ArrayBuffer(size);
var outputArray = new Uint8Array(outputBuffer, 0);
for (var i = 0; i < size; i++) {
outputArray[i] = output[i];
}
return outputArray;
}
}
function Utf8EncodeEnumerator(input)
{
this._input = input;
this._index = -1;
this._buffer = [];
}
Utf8EncodeEnumerator.prototype =
{
current: Number.NaN,
moveNext: function()
{
if (this._buffer.length > 0)
{
this.current = this._buffer.shift();
return true;
}
else if (this._index >= (this._input.length - 1))
{
this.current = Number.NaN;
return false;
}
else
{
var charCode = this._input.charCodeAt(++this._index);
// "\r\n" -> "\n"
//
if ((charCode == 13) && (this._input.charCodeAt(this._index + 1) == 10))
{
charCode = 10;
this._index += 2;
}
if (charCode < 128)
{
this.current = charCode;
}
else if ((charCode > 127) && (charCode < 2048))
{
this.current = (charCode >> 6) | 192;
this._buffer.push((charCode & 63) | 128);
}
else
{
this.current = (charCode >> 12) | 224;
this._buffer.push(((charCode >> 6) & 63) | 128);
this._buffer.push((charCode & 63) | 128);
}
return true;
}
}
}
function Base64DecodeEnumerator(input)
{
this._input = input;
this._index = -1;
this._buffer = [];
}
Base64DecodeEnumerator.prototype =
{
current: 64,
moveNext: function()
{
if (this._buffer.length > 0)
{
this.current = this._buffer.shift();
return true;
}
else if (this._index >= (this._input.length - 1))
{
this.current = 64;
return false;
}
else
{
var enc1 = Base64.codex.indexOf(this._input.charAt(++this._index));
var enc2 = Base64.codex.indexOf(this._input.charAt(++this._index));
var enc3 = Base64.codex.indexOf(this._input.charAt(++this._index));
var enc4 = Base64.codex.indexOf(this._input.charAt(++this._index));
var chr1 = (enc1 << 2) | (enc2 >> 4);
var chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
var chr3 = ((enc3 & 3) << 6) | enc4;
this.current = chr1;
if (enc3 != 64)
this._buffer.push(chr2);
if (enc4 != 64)
this._buffer.push(chr3);
return true;
}
}
};

View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<!--
* Copyright (c) 2009 The Chromium Authors. All rights reserved. Use of this
* source code is governed by a BSD-style license that can be found in the
* LICENSE file.
-->
<html>
<head>
<title>OAuth Redirect Page</title>
<style type="text/css">
body {
font: 16px Arial;
color: #333;
}
</style>
<script type="text/javascript" src="chrome_ex_oauthsimple.js"></script>
<script type="text/javascript" src="chrome_ex_oauth.js"></script>
<script type="text/javascript">
function onLoad() {
ChromeExOAuth.initCallbackPage();
};
</script>
</head>
<body onload="onLoad();">
Redirecting...
</body>
</html>

View File

@@ -0,0 +1,593 @@
/**
* Copyright (c) 2010 The Chromium Authors. All rights reserved. Use of this
* source code is governed by a BSD-style license that can be found in the
* LICENSE file.
*/
/**
* Constructor - no need to invoke directly, call initBackgroundPage instead.
* @constructor
* @param {String} url_request_token The OAuth request token URL.
* @param {String} url_auth_token The OAuth authorize token URL.
* @param {String} url_access_token The OAuth access token URL.
* @param {String} consumer_key The OAuth consumer key.
* @param {String} consumer_secret The OAuth consumer secret.
* @param {String} oauth_scope The OAuth scope parameter.
* @param {Object} opt_args Optional arguments. Recognized parameters:
* "app_name" {String} Name of the current application
* "callback_page" {String} If you renamed chrome_ex_oauth.html, the name
* this file was renamed to.
*/
function ChromeExOAuth(url_request_token, url_auth_token, url_access_token,
consumer_key, consumer_secret, oauth_scope, opt_args) {
this.url_request_token = url_request_token;
this.url_auth_token = url_auth_token;
this.url_access_token = url_access_token;
this.consumer_key = consumer_key;
this.consumer_secret = consumer_secret;
this.oauth_scope = oauth_scope;
this.app_name = opt_args && opt_args['app_name'] ||
"ChromeExOAuth Library";
this.key_token = "oauth_token";
this.key_token_secret = "oauth_token_secret";
this.callback_page = opt_args && opt_args['callback_page'] ||
"chrome_ex_oauth.html";
this.auth_params = {};
if (opt_args && opt_args['auth_params']) {
for (key in opt_args['auth_params']) {
if (opt_args['auth_params'].hasOwnProperty(key)) {
this.auth_params[key] = opt_args['auth_params'][key];
}
}
}
};
/*******************************************************************************
* PUBLIC API METHODS
* Call these from your background page.
******************************************************************************/
/**
* Initializes the OAuth helper from the background page. You must call this
* before attempting to make any OAuth calls.
* @param {Object} oauth_config Configuration parameters in a JavaScript object.
* The following parameters are recognized:
* "request_url" {String} OAuth request token URL.
* "authorize_url" {String} OAuth authorize token URL.
* "access_url" {String} OAuth access token URL.
* "consumer_key" {String} OAuth consumer key.
* "consumer_secret" {String} OAuth consumer secret.
* "scope" {String} OAuth access scope.
* "app_name" {String} Application name.
* "auth_params" {Object} Additional parameters to pass to the
* Authorization token URL. For an example, 'hd', 'hl', 'btmpl':
* http://code.google.com/apis/accounts/docs/OAuth_ref.html#GetAuth
* @return {ChromeExOAuth} An initialized ChromeExOAuth object.
*/
ChromeExOAuth.initBackgroundPage = function(oauth_config) {
window.chromeExOAuthConfig = oauth_config;
window.chromeExOAuth = ChromeExOAuth.fromConfig(oauth_config);
window.chromeExOAuthRedirectStarted = false;
window.chromeExOAuthRequestingAccess = false;
var url_match = chrome.extension.getURL(window.chromeExOAuth.callback_page);
var tabs = {};
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
if (changeInfo.url &&
changeInfo.url.substr(0, url_match.length) === url_match &&
changeInfo.url != tabs[tabId] &&
window.chromeExOAuthRequestingAccess == false) {
chrome.tabs.create({ 'url' : changeInfo.url }, function(tab) {
tabs[tab.id] = tab.url;
chrome.tabs.remove(tabId);
});
}
});
return window.chromeExOAuth;
};
/**
* Authorizes the current user with the configued API. You must call this
* before calling sendSignedRequest.
* @param {Function} callback A function to call once an access token has
* been obtained. This callback will be passed the following arguments:
* token {String} The OAuth access token.
* secret {String} The OAuth access token secret.
*/
ChromeExOAuth.prototype.authorize = function(callback) {
if (this.hasToken()) {
callback(this.getToken(), this.getTokenSecret());
} else {
window.chromeExOAuthOnAuthorize = function(token, secret) {
callback(token, secret);
};
chrome.tabs.create({ 'url' :chrome.extension.getURL(this.callback_page) });
}
};
/**
* Clears any OAuth tokens stored for this configuration. Effectively a
* "logout" of the configured OAuth API.
*/
ChromeExOAuth.prototype.clearTokens = function() {
delete localStorage[this.key_token + encodeURI(this.oauth_scope)];
delete localStorage[this.key_token_secret + encodeURI(this.oauth_scope)];
};
/**
* Returns whether a token is currently stored for this configuration.
* Effectively a check to see whether the current user is "logged in" to
* the configured OAuth API.
* @return {Boolean} True if an access token exists.
*/
ChromeExOAuth.prototype.hasToken = function() {
return !!this.getToken();
};
/**
* Makes an OAuth-signed HTTP request with the currently authorized tokens.
* @param {String} url The URL to send the request to. Querystring parameters
* should be omitted.
* @param {Function} callback A function to be called once the request is
* completed. This callback will be passed the following arguments:
* responseText {String} The text response.
* xhr {XMLHttpRequest} The XMLHttpRequest object which was used to
* send the request. Useful if you need to check response status
* code, etc.
* @param {Object} opt_params Additional parameters to configure the request.
* The following parameters are accepted:
* "method" {String} The HTTP method to use. Defaults to "GET".
* "body" {String} A request body to send. Defaults to null.
* "parameters" {Object} Query parameters to include in the request.
* "headers" {Object} Additional headers to include in the request.
*/
ChromeExOAuth.prototype.sendSignedRequest = function(url, callback,
opt_params) {
var method = opt_params && opt_params['method'] || 'GET';
var body = opt_params && opt_params['body'] || null;
var params = opt_params && opt_params['parameters'] || {};
var headers = opt_params && opt_params['headers'] || {};
var signedUrl = this.signURL(url, method, params);
ChromeExOAuth.sendRequest(method, signedUrl, headers, body, function (xhr) {
if (xhr.readyState == 4) {
callback(xhr.responseText, xhr);
}
});
};
/**
* Adds the required OAuth parameters to the given url and returns the
* result. Useful if you need a signed url but don't want to make an XHR
* request.
* @param {String} method The http method to use.
* @param {String} url The base url of the resource you are querying.
* @param {Object} opt_params Query parameters to include in the request.
* @return {String} The base url plus any query params plus any OAuth params.
*/
ChromeExOAuth.prototype.signURL = function(url, method, opt_params) {
var token = this.getToken();
var secret = this.getTokenSecret();
if (!token || !secret) {
throw new Error("No oauth token or token secret");
}
var params = opt_params || {};
var result = OAuthSimple().sign({
action : method,
path : url,
parameters : params,
signatures: {
consumer_key : this.consumer_key,
shared_secret : this.consumer_secret,
oauth_secret : secret,
oauth_token: token
}
});
return result.signed_url;
};
/**
* Generates the Authorization header based on the oauth parameters.
* @param {String} url The base url of the resource you are querying.
* @param {Object} opt_params Query parameters to include in the request.
* @return {String} An Authorization header containing the oauth_* params.
*/
ChromeExOAuth.prototype.getAuthorizationHeader = function(url, method,
opt_params) {
var token = this.getToken();
var secret = this.getTokenSecret();
if (!token || !secret) {
throw new Error("No oauth token or token secret");
}
var params = opt_params || {};
return OAuthSimple().getHeaderString({
action: method,
path : url,
parameters : params,
signatures: {
consumer_key : this.consumer_key,
shared_secret : this.consumer_secret,
oauth_secret : secret,
oauth_token: token
}
});
};
/*******************************************************************************
* PRIVATE API METHODS
* Used by the library. There should be no need to call these methods directly.
******************************************************************************/
/**
* Creates a new ChromeExOAuth object from the supplied configuration object.
* @param {Object} oauth_config Configuration parameters in a JavaScript object.
* The following parameters are recognized:
* "request_url" {String} OAuth request token URL.
* "authorize_url" {String} OAuth authorize token URL.
* "access_url" {String} OAuth access token URL.
* "consumer_key" {String} OAuth consumer key.
* "consumer_secret" {String} OAuth consumer secret.
* "scope" {String} OAuth access scope.
* "app_name" {String} Application name.
* "auth_params" {Object} Additional parameters to pass to the
* Authorization token URL. For an example, 'hd', 'hl', 'btmpl':
* http://code.google.com/apis/accounts/docs/OAuth_ref.html#GetAuth
* @return {ChromeExOAuth} An initialized ChromeExOAuth object.
*/
ChromeExOAuth.fromConfig = function(oauth_config) {
return new ChromeExOAuth(
oauth_config['request_url'],
oauth_config['authorize_url'],
oauth_config['access_url'],
oauth_config['consumer_key'],
oauth_config['consumer_secret'],
oauth_config['scope'],
{
'app_name' : oauth_config['app_name'],
'auth_params' : oauth_config['auth_params']
}
);
};
/**
* Initializes chrome_ex_oauth.html and redirects the page if needed to start
* the OAuth flow. Once an access token is obtained, this function closes
* chrome_ex_oauth.html.
*/
ChromeExOAuth.initCallbackPage = function() {
var background_page = chrome.extension.getBackgroundPage();
var oauth_config = background_page.chromeExOAuthConfig;
var oauth = ChromeExOAuth.fromConfig(oauth_config);
background_page.chromeExOAuthRedirectStarted = true;
oauth.initOAuthFlow(function (token, secret) {
background_page.chromeExOAuthOnAuthorize(token, secret);
background_page.chromeExOAuthRedirectStarted = false;
chrome.tabs.getSelected(null, function (tab) {
chrome.tabs.remove(tab.id);
});
});
};
/**
* Sends an HTTP request. Convenience wrapper for XMLHttpRequest calls.
* @param {String} method The HTTP method to use.
* @param {String} url The URL to send the request to.
* @param {Object} headers Optional request headers in key/value format.
* @param {String} body Optional body content.
* @param {Function} callback Function to call when the XMLHttpRequest's
* ready state changes. See documentation for XMLHttpRequest's
* onreadystatechange handler for more information.
*/
ChromeExOAuth.sendRequest = function(method, url, headers, body, callback) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(data) {
callback(xhr, data);
}
xhr.open(method, url, true);
if (headers) {
for (var header in headers) {
if (headers.hasOwnProperty(header)) {
xhr.setRequestHeader(header, headers[header]);
}
}
}
xhr.send(body);
};
/**
* Decodes a URL-encoded string into key/value pairs.
* @param {String} encoded An URL-encoded string.
* @return {Object} An object representing the decoded key/value pairs found
* in the encoded string.
*/
ChromeExOAuth.formDecode = function(encoded) {
var params = encoded.split("&");
var decoded = {};
for (var i = 0, param; param = params[i]; i++) {
var keyval = param.split("=");
if (keyval.length == 2) {
var key = ChromeExOAuth.fromRfc3986(keyval[0]);
var val = ChromeExOAuth.fromRfc3986(keyval[1]);
decoded[key] = val;
}
}
return decoded;
};
/**
* Returns the current window's querystring decoded into key/value pairs.
* @return {Object} A object representing any key/value pairs found in the
* current window's querystring.
*/
ChromeExOAuth.getQueryStringParams = function() {
var urlparts = window.location.href.split("?");
if (urlparts.length >= 2) {
var querystring = urlparts.slice(1).join("?");
return ChromeExOAuth.formDecode(querystring);
}
return {};
};
/**
* Binds a function call to a specific object. This function will also take
* a variable number of additional arguments which will be prepended to the
* arguments passed to the bound function when it is called.
* @param {Function} func The function to bind.
* @param {Object} obj The object to bind to the function's "this".
* @return {Function} A closure that will call the bound function.
*/
ChromeExOAuth.bind = function(func, obj) {
var newargs = Array.prototype.slice.call(arguments).slice(2);
return function() {
var combinedargs = newargs.concat(Array.prototype.slice.call(arguments));
func.apply(obj, combinedargs);
};
};
/**
* Encodes a value according to the RFC3986 specification.
* @param {String} val The string to encode.
*/
ChromeExOAuth.toRfc3986 = function(val){
return encodeURIComponent(val)
.replace(/\!/g, "%21")
.replace(/\*/g, "%2A")
.replace(/'/g, "%27")
.replace(/\(/g, "%28")
.replace(/\)/g, "%29");
};
/**
* Decodes a string that has been encoded according to RFC3986.
* @param {String} val The string to decode.
*/
ChromeExOAuth.fromRfc3986 = function(val){
var tmp = val
.replace(/%21/g, "!")
.replace(/%2A/g, "*")
.replace(/%27/g, "'")
.replace(/%28/g, "(")
.replace(/%29/g, ")");
return decodeURIComponent(tmp);
};
/**
* Adds a key/value parameter to the supplied URL.
* @param {String} url An URL which may or may not contain querystring values.
* @param {String} key A key
* @param {String} value A value
* @return {String} The URL with URL-encoded versions of the key and value
* appended, prefixing them with "&" or "?" as needed.
*/
ChromeExOAuth.addURLParam = function(url, key, value) {
var sep = (url.indexOf('?') >= 0) ? "&" : "?";
return url + sep +
ChromeExOAuth.toRfc3986(key) + "=" + ChromeExOAuth.toRfc3986(value);
};
/**
* Stores an OAuth token for the configured scope.
* @param {String} token The token to store.
*/
ChromeExOAuth.prototype.setToken = function(token) {
localStorage[this.key_token + encodeURI(this.oauth_scope)] = token;
};
/**
* Retrieves any stored token for the configured scope.
* @return {String} The stored token.
*/
ChromeExOAuth.prototype.getToken = function() {
return localStorage[this.key_token + encodeURI(this.oauth_scope)];
};
/**
* Stores an OAuth token secret for the configured scope.
* @param {String} secret The secret to store.
*/
ChromeExOAuth.prototype.setTokenSecret = function(secret) {
localStorage[this.key_token_secret + encodeURI(this.oauth_scope)] = secret;
};
/**
* Retrieves any stored secret for the configured scope.
* @return {String} The stored secret.
*/
ChromeExOAuth.prototype.getTokenSecret = function() {
return localStorage[this.key_token_secret + encodeURI(this.oauth_scope)];
};
/**
* Starts an OAuth authorization flow for the current page. If a token exists,
* no redirect is needed and the supplied callback is called immediately.
* If this method detects that a redirect has finished, it grabs the
* appropriate OAuth parameters from the URL and attempts to retrieve an
* access token. If no token exists and no redirect has happened, then
* an access token is requested and the page is ultimately redirected.
* @param {Function} callback The function to call once the flow has finished.
* This callback will be passed the following arguments:
* token {String} The OAuth access token.
* secret {String} The OAuth access token secret.
*/
ChromeExOAuth.prototype.initOAuthFlow = function(callback) {
if (!this.hasToken()) {
var params = ChromeExOAuth.getQueryStringParams();
if (params['chromeexoauthcallback'] == 'true') {
var oauth_token = params['oauth_token'];
var oauth_verifier = params['oauth_verifier']
this.getAccessToken(oauth_token, oauth_verifier, callback);
} else {
var request_params = {
'url_callback_param' : 'chromeexoauthcallback'
}
this.getRequestToken(function(url) {
window.location.href = url;
}, request_params);
}
} else {
callback(this.getToken(), this.getTokenSecret());
}
};
/**
* Requests an OAuth request token.
* @param {Function} callback Function to call once the authorize URL is
* calculated. This callback will be passed the following arguments:
* url {String} The URL the user must be redirected to in order to
* approve the token.
* @param {Object} opt_args Optional arguments. The following parameters
* are accepted:
* "url_callback" {String} The URL the OAuth provider will redirect to.
* "url_callback_param" {String} A parameter to include in the callback
* URL in order to indicate to this library that a redirect has
* taken place.
*/
ChromeExOAuth.prototype.getRequestToken = function(callback, opt_args) {
if (typeof callback !== "function") {
throw new Error("Specified callback must be a function.");
}
var url = opt_args && opt_args['url_callback'] ||
window && window.top && window.top.location &&
window.top.location.href;
var url_param = opt_args && opt_args['url_callback_param'] ||
"chromeexoauthcallback";
var url_callback = ChromeExOAuth.addURLParam(url, url_param, "true");
var result = OAuthSimple().sign({
path : this.url_request_token,
parameters: {
"xoauth_displayname" : this.app_name,
"scope" : this.oauth_scope,
"oauth_callback" : url_callback
},
signatures: {
consumer_key : this.consumer_key,
shared_secret : this.consumer_secret
}
});
var onToken = ChromeExOAuth.bind(this.onRequestToken, this, callback);
ChromeExOAuth.sendRequest("GET", result.signed_url, null, null, onToken);
};
/**
* Called when a request token has been returned. Stores the request token
* secret for later use and sends the authorization url to the supplied
* callback (for redirecting the user).
* @param {Function} callback Function to call once the authorize URL is
* calculated. This callback will be passed the following arguments:
* url {String} The URL the user must be redirected to in order to
* approve the token.
* @param {XMLHttpRequest} xhr The XMLHttpRequest object used to fetch the
* request token.
*/
ChromeExOAuth.prototype.onRequestToken = function(callback, xhr) {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
var params = ChromeExOAuth.formDecode(xhr.responseText);
var token = params['oauth_token'];
this.setTokenSecret(params['oauth_token_secret']);
var url = ChromeExOAuth.addURLParam(this.url_auth_token,
"oauth_token", token);
for (var key in this.auth_params) {
if (this.auth_params.hasOwnProperty(key)) {
url = ChromeExOAuth.addURLParam(url, key, this.auth_params[key]);
}
}
callback(url);
} else {
throw new Error("Fetching request token failed. Status " + xhr.status);
}
}
};
/**
* Requests an OAuth access token.
* @param {String} oauth_token The OAuth request token.
* @param {String} oauth_verifier The OAuth token verifier.
* @param {Function} callback The function to call once the token is obtained.
* This callback will be passed the following arguments:
* token {String} The OAuth access token.
* secret {String} The OAuth access token secret.
*/
ChromeExOAuth.prototype.getAccessToken = function(oauth_token, oauth_verifier,
callback) {
if (typeof callback !== "function") {
throw new Error("Specified callback must be a function.");
}
var bg = chrome.extension.getBackgroundPage();
if (bg.chromeExOAuthRequestingAccess == false) {
bg.chromeExOAuthRequestingAccess = true;
var result = OAuthSimple().sign({
path : this.url_access_token,
parameters: {
"oauth_token" : oauth_token,
"oauth_verifier" : oauth_verifier
},
signatures: {
consumer_key : this.consumer_key,
shared_secret : this.consumer_secret,
oauth_secret : this.getTokenSecret(this.oauth_scope)
}
});
var onToken = ChromeExOAuth.bind(this.onAccessToken, this, callback);
ChromeExOAuth.sendRequest("GET", result.signed_url, null, null, onToken);
}
};
/**
* Called when an access token has been returned. Stores the access token and
* access token secret for later use and sends them to the supplied callback.
* @param {Function} callback The function to call once the token is obtained.
* This callback will be passed the following arguments:
* token {String} The OAuth access token.
* secret {String} The OAuth access token secret.
* @param {XMLHttpRequest} xhr The XMLHttpRequest object used to fetch the
* access token.
*/
ChromeExOAuth.prototype.onAccessToken = function(callback, xhr) {
if (xhr.readyState == 4) {
var bg = chrome.extension.getBackgroundPage();
if (xhr.status == 200) {
var params = ChromeExOAuth.formDecode(xhr.responseText);
var token = params["oauth_token"];
var secret = params["oauth_token_secret"];
this.setToken(token);
this.setTokenSecret(secret);
bg.chromeExOAuthRequestingAccess = false;
callback(token, secret);
} else {
bg.chromeExOAuthRequestingAccess = false;
throw new Error("Fetching access token failed with status " + xhr.status);
}
}
};

View File

@@ -0,0 +1,458 @@
/* OAuthSimple
* A simpler version of OAuth
*
* author: jr conlin
* mail: src@anticipatr.com
* copyright: unitedHeroes.net
* version: 1.0
* url: http://unitedHeroes.net/OAuthSimple
*
* Copyright (c) 2009, unitedHeroes.net
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the unitedHeroes.net nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY UNITEDHEROES.NET ''AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL UNITEDHEROES.NET BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
var OAuthSimple;
if (OAuthSimple === undefined)
{
/* Simple OAuth
*
* This class only builds the OAuth elements, it does not do the actual
* transmission or reception of the tokens. It does not validate elements
* of the token. It is for client use only.
*
* api_key is the API key, also known as the OAuth consumer key
* shared_secret is the shared secret (duh).
*
* Both the api_key and shared_secret are generally provided by the site
* offering OAuth services. You need to specify them at object creation
* because nobody <explative>ing uses OAuth without that minimal set of
* signatures.
*
* If you want to use the higher order security that comes from the
* OAuth token (sorry, I don't provide the functions to fetch that because
* sites aren't horribly consistent about how they offer that), you need to
* pass those in either with .setTokensAndSecrets() or as an argument to the
* .sign() or .getHeaderString() functions.
*
* Example:
<code>
var oauthObject = OAuthSimple().sign({path:'http://example.com/rest/',
parameters: 'foo=bar&gorp=banana',
signatures:{
api_key:'12345abcd',
shared_secret:'xyz-5309'
}});
document.getElementById('someLink').href=oauthObject.signed_url;
</code>
*
* that will sign as a "GET" using "SHA1-MAC" the url. If you need more than
* that, read on, McDuff.
*/
/** OAuthSimple creator
*
* Create an instance of OAuthSimple
*
* @param api_key {string} The API Key (sometimes referred to as the consumer key) This value is usually supplied by the site you wish to use.
* @param shared_secret (string) The shared secret. This value is also usually provided by the site you wish to use.
*/
OAuthSimple = function (consumer_key,shared_secret)
{
/* if (api_key == undefined)
throw("Missing argument: api_key (oauth_consumer_key) for OAuthSimple. This is usually provided by the hosting site.");
if (shared_secret == undefined)
throw("Missing argument: shared_secret (shared secret) for OAuthSimple. This is usually provided by the hosting site.");
*/ this._secrets={};
this._parameters={};
// General configuration options.
if (consumer_key !== undefined) {
this._secrets['consumer_key'] = consumer_key;
}
if (shared_secret !== undefined) {
this._secrets['shared_secret'] = shared_secret;
}
this._default_signature_method= "HMAC-SHA1";
this._action = "GET";
this._nonce_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
this.reset = function() {
this._parameters={};
this._path=undefined;
return this;
};
/** set the parameters either from a hash or a string
*
* @param {string,object} List of parameters for the call, this can either be a URI string (e.g. "foo=bar&gorp=banana" or an object/hash)
*/
this.setParameters = function (parameters) {
if (parameters === undefined) {
parameters = {};
}
if (typeof(parameters) == 'string') {
parameters=this._parseParameterString(parameters);
}
this._parameters = parameters;
if (this._parameters['oauth_nonce'] === undefined) {
this._getNonce();
}
if (this._parameters['oauth_timestamp'] === undefined) {
this._getTimestamp();
}
if (this._parameters['oauth_method'] === undefined) {
this.setSignatureMethod();
}
if (this._parameters['oauth_consumer_key'] === undefined) {
this._getApiKey();
}
if(this._parameters['oauth_token'] === undefined) {
this._getAccessToken();
}
return this;
};
/** convienence method for setParameters
*
* @param parameters {string,object} See .setParameters
*/
this.setQueryString = function (parameters) {
return this.setParameters(parameters);
};
/** Set the target URL (does not include the parameters)
*
* @param path {string} the fully qualified URI (excluding query arguments) (e.g "http://example.org/foo")
*/
this.setURL = function (path) {
if (path == '') {
throw ('No path specified for OAuthSimple.setURL');
}
this._path = path;
return this;
};
/** convienence method for setURL
*
* @param path {string} see .setURL
*/
this.setPath = function(path){
return this.setURL(path);
};
/** set the "action" for the url, (e.g. GET,POST, DELETE, etc.)
*
* @param action {string} HTTP Action word.
*/
this.setAction = function(action) {
if (action === undefined) {
action="GET";
}
action = action.toUpperCase();
if (action.match('[^A-Z]')) {
throw ('Invalid action specified for OAuthSimple.setAction');
}
this._action = action;
return this;
};
/** set the signatures (as well as validate the ones you have)
*
* @param signatures {object} object/hash of the token/signature pairs {api_key:, shared_secret:, oauth_token: oauth_secret:}
*/
this.setTokensAndSecrets = function(signatures) {
if (signatures)
{
for (var i in signatures) {
this._secrets[i] = signatures[i];
}
}
// Aliases
if (this._secrets['api_key']) {
this._secrets.consumer_key = this._secrets.api_key;
}
if (this._secrets['access_token']) {
this._secrets.oauth_token = this._secrets.access_token;
}
if (this._secrets['access_secret']) {
this._secrets.oauth_secret = this._secrets.access_secret;
}
// Gauntlet
if (this._secrets.consumer_key === undefined) {
throw('Missing required consumer_key in OAuthSimple.setTokensAndSecrets');
}
if (this._secrets.shared_secret === undefined) {
throw('Missing required shared_secret in OAuthSimple.setTokensAndSecrets');
}
if ((this._secrets.oauth_token !== undefined) && (this._secrets.oauth_secret === undefined)) {
throw('Missing oauth_secret for supplied oauth_token in OAuthSimple.setTokensAndSecrets');
}
return this;
};
/** set the signature method (currently only Plaintext or SHA-MAC1)
*
* @param method {string} Method of signing the transaction (only PLAINTEXT and SHA-MAC1 allowed for now)
*/
this.setSignatureMethod = function(method) {
if (method === undefined) {
method = this._default_signature_method;
}
//TODO: accept things other than PlainText or SHA-MAC1
if (method.toUpperCase().match(/(PLAINTEXT|HMAC-SHA1)/) === undefined) {
throw ('Unknown signing method specified for OAuthSimple.setSignatureMethod');
}
this._parameters['oauth_signature_method']= method.toUpperCase();
return this;
};
/** sign the request
*
* note: all arguments are optional, provided you've set them using the
* other helper functions.
*
* @param args {object} hash of arguments for the call
* {action:, path:, parameters:, method:, signatures:}
* all arguments are optional.
*/
this.sign = function (args) {
if (args === undefined) {
args = {};
}
// Set any given parameters
if(args['action'] !== undefined) {
this.setAction(args['action']);
}
if (args['path'] !== undefined) {
this.setPath(args['path']);
}
if (args['method'] !== undefined) {
this.setSignatureMethod(args['method']);
}
this.setTokensAndSecrets(args['signatures']);
if (args['parameters'] !== undefined){
this.setParameters(args['parameters']);
}
// check the parameters
var normParams = this._normalizedParameters();
this._parameters['oauth_signature']=this._generateSignature(normParams);
return {
parameters: this._parameters,
signature: this._oauthEscape(this._parameters['oauth_signature']),
signed_url: this._path + '?' + this._normalizedParameters(),
header: this.getHeaderString()
};
};
/** Return a formatted "header" string
*
* NOTE: This doesn't set the "Authorization: " prefix, which is required.
* I don't set it because various set header functions prefer different
* ways to do that.
*
* @param args {object} see .sign
*/
this.getHeaderString = function(args) {
if (this._parameters['oauth_signature'] === undefined) {
this.sign(args);
}
var result = 'OAuth ';
for (var pName in this._parameters)
{
if (!pName.match(/^oauth/)) {
continue;
}
if ((this._parameters[pName]) instanceof Array)
{
var pLength = this._parameters[pName].length;
for (var j=0;j<pLength;j++)
{
result += pName +'="'+this._oauthEscape(this._parameters[pName][j])+'" ';
}
}
else
{
result += pName + '="'+this._oauthEscape(this._parameters[pName])+'" ';
}
}
return result;
};
// Start Private Methods.
/** convert the parameter string into a hash of objects.
*
*/
this._parseParameterString = function(paramString){
var elements = paramString.split('&');
var result={};
for(var element=elements.shift();element;element=elements.shift())
{
var keyToken=element.split('=');
var value='';
if (keyToken[1]) {
value=decodeURIComponent(keyToken[1]);
}
if(result[keyToken[0]]){
if (!(result[keyToken[0]] instanceof Array))
{
result[keyToken[0]] = Array(result[keyToken[0]],value);
}
else
{
result[keyToken[0]].push(value);
}
}
else
{
result[keyToken[0]]=value;
}
}
return result;
};
this._oauthEscape = function(string) {
if (string === undefined) {
return "";
}
if (string instanceof Array)
{
throw('Array passed to _oauthEscape');
}
return encodeURIComponent(string).replace(/\!/g, "%21").
replace(/\*/g, "%2A").
replace(/'/g, "%27").
replace(/\(/g, "%28").
replace(/\)/g, "%29");
};
this._getNonce = function (length) {
if (length === undefined) {
length=5;
}
var result = "";
var cLength = this._nonce_chars.length;
for (var i = 0; i < length;i++) {
var rnum = Math.floor(Math.random() *cLength);
result += this._nonce_chars.substring(rnum,rnum+1);
}
this._parameters['oauth_nonce']=result;
return result;
};
this._getApiKey = function() {
if (this._secrets.consumer_key === undefined) {
throw('No consumer_key set for OAuthSimple.');
}
this._parameters['oauth_consumer_key']=this._secrets.consumer_key;
return this._parameters.oauth_consumer_key;
};
this._getAccessToken = function() {
if (this._secrets['oauth_secret'] === undefined) {
return '';
}
if (this._secrets['oauth_token'] === undefined) {
throw('No oauth_token (access_token) set for OAuthSimple.');
}
this._parameters['oauth_token'] = this._secrets.oauth_token;
return this._parameters.oauth_token;
};
this._getTimestamp = function() {
var d = new Date();
var ts = Math.floor(d.getTime()/1000);
this._parameters['oauth_timestamp'] = ts;
return ts;
};
this.b64_hmac_sha1 = function(k,d,_p,_z){
// heavily optimized and compressed version of http://pajhome.org.uk/crypt/md5/sha1.js
// _p = b64pad, _z = character size; not used here but I left them available just in case
if(!_p){_p='=';}if(!_z){_z=8;}function _f(t,b,c,d){if(t<20){return(b&c)|((~b)&d);}if(t<40){return b^c^d;}if(t<60){return(b&c)|(b&d)|(c&d);}return b^c^d;}function _k(t){return(t<20)?1518500249:(t<40)?1859775393:(t<60)?-1894007588:-899497514;}function _s(x,y){var l=(x&0xFFFF)+(y&0xFFFF),m=(x>>16)+(y>>16)+(l>>16);return(m<<16)|(l&0xFFFF);}function _r(n,c){return(n<<c)|(n>>>(32-c));}function _c(x,l){x[l>>5]|=0x80<<(24-l%32);x[((l+64>>9)<<4)+15]=l;var w=[80],a=1732584193,b=-271733879,c=-1732584194,d=271733878,e=-1009589776;for(var i=0;i<x.length;i+=16){var o=a,p=b,q=c,r=d,s=e;for(var j=0;j<80;j++){if(j<16){w[j]=x[i+j];}else{w[j]=_r(w[j-3]^w[j-8]^w[j-14]^w[j-16],1);}var t=_s(_s(_r(a,5),_f(j,b,c,d)),_s(_s(e,w[j]),_k(j)));e=d;d=c;c=_r(b,30);b=a;a=t;}a=_s(a,o);b=_s(b,p);c=_s(c,q);d=_s(d,r);e=_s(e,s);}return[a,b,c,d,e];}function _b(s){var b=[],m=(1<<_z)-1;for(var i=0;i<s.length*_z;i+=_z){b[i>>5]|=(s.charCodeAt(i/8)&m)<<(32-_z-i%32);}return b;}function _h(k,d){var b=_b(k);if(b.length>16){b=_c(b,k.length*_z);}var p=[16],o=[16];for(var i=0;i<16;i++){p[i]=b[i]^0x36363636;o[i]=b[i]^0x5C5C5C5C;}var h=_c(p.concat(_b(d)),512+d.length*_z);return _c(o.concat(h),512+160);}function _n(b){var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",s='';for(var i=0;i<b.length*4;i+=3){var r=(((b[i>>2]>>8*(3-i%4))&0xFF)<<16)|(((b[i+1>>2]>>8*(3-(i+1)%4))&0xFF)<<8)|((b[i+2>>2]>>8*(3-(i+2)%4))&0xFF);for(var j=0;j<4;j++){if(i*8+j*6>b.length*32){s+=_p;}else{s+=t.charAt((r>>6*(3-j))&0x3F);}}}return s;}function _x(k,d){return _n(_h(k,d));}return _x(k,d);
}
this._normalizedParameters = function() {
var elements = new Array();
var paramNames = [];
var ra =0;
for (var paramName in this._parameters)
{
if (ra++ > 1000) {
throw('runaway 1');
}
paramNames.unshift(paramName);
}
paramNames = paramNames.sort();
pLen = paramNames.length;
for (var i=0;i<pLen; i++)
{
paramName=paramNames[i];
//skip secrets.
if (paramName.match(/\w+_secret/)) {
continue;
}
if (this._parameters[paramName] instanceof Array)
{
var sorted = this._parameters[paramName].sort();
var spLen = sorted.length;
for (var j = 0;j<spLen;j++){
if (ra++ > 1000) {
throw('runaway 1');
}
elements.push(this._oauthEscape(paramName) + '=' +
this._oauthEscape(sorted[j]));
}
continue;
}
elements.push(this._oauthEscape(paramName) + '=' +
this._oauthEscape(this._parameters[paramName]));
}
return elements.join('&');
};
this._generateSignature = function() {
var secretKey = this._oauthEscape(this._secrets.shared_secret)+'&'+
this._oauthEscape(this._secrets.oauth_secret);
if (this._parameters['oauth_signature_method'] == 'PLAINTEXT')
{
return secretKey;
}
if (this._parameters['oauth_signature_method'] == 'HMAC-SHA1')
{
var sigString = this._oauthEscape(this._action)+'&'+this._oauthEscape(this._path)+'&'+this._oauthEscape(this._normalizedParameters());
return this.b64_hmac_sha1(secretKey,sigString);
}
return null;
};
return this;
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

Some files were not shown because too many files have changed in this diff Show More