├── .gitignore
├── AndroidManifest.xml
├── README.md
├── ant.properties
├── build.xml
├── libs
└── android-support-v4.jar
├── project.properties
├── res
├── layout
│ └── main.xml
├── values-v11
│ └── styles.xml
└── values
│ ├── strings.xml
│ └── styles.xml
└── src
└── net
└── mlcastle
└── photospy
├── BitmapLoader.java
├── ShowImage.java
└── SpyActivity.java
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | bin
3 | gen
4 | *.iml
5 | local.properties
6 |
--------------------------------------------------------------------------------
/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
10 |
11 |
12 |
13 |
14 |
15 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Photo Spy for Android
2 | =====================
3 |
4 | This is a small program to display the list of photos on your Android
5 | device and then to display those photos to you. The interesting thing
6 | is that it requires absolutely no system permissions to be able to do
7 | so.
8 |
9 | This app does not have [`INTERNET` permission][internet]. So it can
10 | not send your images to anyone and is not a “true” spy. But given that
11 | almost every Android app in the Market does in fact have `INTERNET`
12 | permission, this is not much of a limitation.
13 |
14 | According [an article][nyt] in the _Times_, iOS needs location permission
15 | to be able to do the same, which is kind of confusing. Google
16 | “declined to comment on how its Android operating system for mobile
17 | devices handles this issue,” which is kind of wise under the
18 | circumstances.
19 |
20 | **Note:** as of Android 4.4 ("KitKat"), this [no longer works][kk] without
21 | the `READ_EXTERNAL_STORAGE` permission.
22 |
23 | Installation
24 | ------------
25 |
26 | Scan this QR code:
27 |
28 | ![QR Code][qrcode]
29 |
30 |
31 | Building
32 | --------
33 |
34 | Get the [Android SDK][sdk]. Then, fetch a copy of this project, open
35 | up a terminal in the project's directory, and do
36 |
37 | ```
38 | android update project -p .
39 | ant clean debug
40 | ```
41 |
42 | If you have a phone plugged in or an emulator running, you can then do
43 |
44 | ```
45 | ant installd
46 | ```
47 |
48 | to install.
49 |
50 | License
51 | -------
52 |
53 | Copyright © 2012 [Michael Castleman][me].
54 |
55 | This program is free software. It comes without any warranty, to
56 | the extent permitted by applicable law. You can redistribute it
57 | and/or modify it under the terms of the [Do What The Fuck You Want
58 | To Public License][wtfpl], Version 2, as published by Sam Hocevar.
59 |
60 | This project uses and distributes the [Android Compatibility
61 | Library][support], which is Copyright The Android Open Source Project
62 | and distributed under the [Apache License, Version 2.0][apache].
63 |
64 | [internet]: http://developer.android.com/reference/android/Manifest.permission.html#INTERNET
65 | [nyt]: http://bits.blogs.nytimes.com/2012/02/28/tk-ios-gives-developers-access-to-photos-videos-location/?hp
66 | [qrcode]: https://chart.googleapis.com/chart?cht=qr&chs=300x300&chl=https://github.com/downloads/mlc/android-photo-spy/PhotoSpy-debug.apk
67 | [sdk]: http://developer.android.com/sdk/index.html
68 | [me]: http://mlcastle.net/
69 | [wtfpl]: http://sam.zoy.org/wtfpl/
70 | [support]: http://developer.android.com/sdk/compatibility-library.html
71 | [apache]: https://www.apache.org/licenses/LICENSE-2.0
72 | [kk]: https://developer.android.com/about/versions/android-4.4.html#BehaviorStorage
73 |
--------------------------------------------------------------------------------
/ant.properties:
--------------------------------------------------------------------------------
1 | # This file is used to override default values used by the Ant build system.
2 | #
3 | # This file must be checked in Version Control Systems, as it is
4 | # integral to the build system of your project.
5 |
6 | # This file is only used by the Ant script.
7 |
8 | # You can use this to override default values such as
9 | # 'source.dir' for the location of your java source folder and
10 | # 'out.dir' for the location of your output folder.
11 |
12 | # You can also use it define how the release builds are signed by declaring
13 | # the following properties:
14 | # 'key.store' for the location of your keystore and
15 | # 'key.alias' for the name of the key to use.
16 | # The password will be asked during the build when you use the 'release' target.
17 |
18 |
--------------------------------------------------------------------------------
/build.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
29 |
30 |
31 |
40 |
41 |
42 |
43 |
47 |
48 |
49 |
51 |
63 |
64 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/libs/android-support-v4.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mlc/android-photo-spy/8c6b018973d7a91893af4862df338afa35fb3495/libs/android-support-v4.jar
--------------------------------------------------------------------------------
/project.properties:
--------------------------------------------------------------------------------
1 | # This file is automatically generated by Android Tools.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file must be checked in Version Control Systems.
5 | #
6 | # To customize properties used by the Ant build system use,
7 | # "ant.properties", and override values to adapt the script to your
8 | # project structure.
9 |
10 | # Project target.
11 | target=android-14
12 |
--------------------------------------------------------------------------------
/res/layout/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
11 |
--------------------------------------------------------------------------------
/res/values-v11/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Photo Spy
4 | Image
5 | unknown location
6 |
7 |
--------------------------------------------------------------------------------
/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/net/mlcastle/photospy/BitmapLoader.java:
--------------------------------------------------------------------------------
1 | package net.mlcastle.photospy;
2 |
3 | import java.io.InputStream;
4 | import android.content.ContentResolver;
5 | import android.content.Context;
6 | import android.graphics.Bitmap;
7 | import android.graphics.BitmapFactory;
8 | import android.net.Uri;
9 | import android.support.v4.content.AsyncTaskLoader;
10 |
11 | public class BitmapLoader extends AsyncTaskLoader {
12 | private final Uri uri;
13 | private final int dimen;
14 |
15 | public BitmapLoader(Context ctx, Uri uri, int dimen) {
16 | super(ctx);
17 | this.uri = uri;
18 | this.dimen = dimen;
19 | }
20 |
21 | @Override
22 | public Bitmap loadInBackground() {
23 | final ContentResolver res = getContext().getContentResolver();
24 | BitmapFactory.Options opts = new BitmapFactory.Options();
25 | opts.inJustDecodeBounds = true;
26 | read(res, uri, opts);
27 |
28 | int imgdimen = Math.max(opts.outHeight, opts.outWidth);
29 | opts = new BitmapFactory.Options();
30 | opts.inSampleSize = 1;
31 | while (imgdimen > dimen) {
32 | imgdimen /= 2;
33 | opts.inSampleSize <<= 1;
34 | }
35 | opts.inSampleSize >>= 1;
36 | return read(res, uri, opts);
37 | }
38 |
39 | @Override
40 | protected void onStartLoading() {
41 | forceLoad();
42 | }
43 |
44 | private static Bitmap read(ContentResolver res, Uri uri, BitmapFactory.Options opts) {
45 | InputStream in = null;
46 | try {
47 | in = res.openInputStream(uri);
48 | return BitmapFactory.decodeStream(in, null, opts);
49 | } catch (Exception ex) {
50 | throw new RuntimeException(ex);
51 | } finally {
52 | if (in != null) {
53 | try {
54 | in.close();
55 | } catch (Exception ignore) { }
56 | }
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/net/mlcastle/photospy/ShowImage.java:
--------------------------------------------------------------------------------
1 | package net.mlcastle.photospy;
2 |
3 | import android.graphics.Bitmap;
4 | import android.net.Uri;
5 | import android.os.Bundle;
6 | import android.support.v4.app.FragmentActivity;
7 | import android.support.v4.app.LoaderManager;
8 | import android.support.v4.content.Loader;
9 | import android.util.DisplayMetrics;
10 | import android.view.ViewGroup;
11 | import android.widget.FrameLayout;
12 | import android.widget.ImageView;
13 |
14 | public class ShowImage extends FragmentActivity implements LoaderManager.LoaderCallbacks {
15 | private ImageView iv;
16 |
17 | @Override
18 | protected void onCreate(Bundle savedInstanceState) {
19 | super.onCreate(savedInstanceState);
20 | iv = new ImageView(this);
21 | FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT);
22 | iv.setLayoutParams(lp);
23 | iv.setScaleType(ImageView.ScaleType.FIT_CENTER);
24 | setContentView(iv);
25 |
26 | Bundle args = new Bundle();
27 | args.putParcelable("uri", getIntent().getData());
28 | getSupportLoaderManager().initLoader(1, args, this);
29 | }
30 |
31 | public Loader onCreateLoader(int i, Bundle args) {
32 | DisplayMetrics metrics = new DisplayMetrics();
33 | getWindowManager().getDefaultDisplay().getMetrics(metrics);
34 | return new BitmapLoader(this, (Uri)args.getParcelable("uri"), Math.max(metrics.heightPixels, metrics.widthPixels));
35 | }
36 |
37 | public void onLoadFinished(Loader loader, Bitmap bitmap) {
38 | iv.setImageBitmap(bitmap);
39 | }
40 |
41 | public void onLoaderReset(Loader loader) {
42 | iv.setImageBitmap(null);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/net/mlcastle/photospy/SpyActivity.java:
--------------------------------------------------------------------------------
1 | package net.mlcastle.photospy;
2 |
3 | import android.content.Intent;
4 | import android.database.Cursor;
5 | import android.net.Uri;
6 | import android.os.Bundle;
7 | import android.provider.MediaStore;
8 | import android.support.v4.app.FragmentActivity;
9 | import android.support.v4.app.LoaderManager;
10 | import android.support.v4.content.CursorLoader;
11 | import android.support.v4.content.Loader;
12 | import android.support.v4.widget.SimpleCursorAdapter;
13 | import android.util.Log;
14 | import android.view.View;
15 | import android.widget.AdapterView;
16 | import android.widget.ListView;
17 | import android.widget.TextView;
18 |
19 | public class SpyActivity extends FragmentActivity implements LoaderManager.LoaderCallbacks,AdapterView.OnItemClickListener {
20 | private static final String TAG = "SpyActivity";
21 | private static final String[] PROJECTION = {
22 | MediaStore.Images.Media.DISPLAY_NAME,
23 | MediaStore.Images.Media.LATITUDE,
24 | MediaStore.Images.Media.LONGITUDE,
25 | MediaStore.Images.Media._ID
26 | };
27 | private ListView list;
28 | private View empty;
29 | private SimpleCursorAdapter adapter;
30 |
31 | /** Called when the activity is first created. */
32 | @Override
33 | public void onCreate(Bundle savedInstanceState)
34 | {
35 | super.onCreate(savedInstanceState);
36 | setContentView(R.layout.main);
37 |
38 | list = (ListView)findViewById(android.R.id.list);
39 | empty = findViewById(android.R.id.empty);
40 |
41 | list.setVisibility(View.GONE);
42 | empty.setVisibility(View.VISIBLE);
43 |
44 | adapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_2, null, PROJECTION, new int[] { android.R.id.text1, android.R.id.text2 }, 0);
45 | adapter.setViewBinder(new TitleLocationBinder());
46 | list.setAdapter(adapter);
47 | list.setOnItemClickListener(this);
48 | getSupportLoaderManager().initLoader(0, null, this);
49 | }
50 |
51 | public Loader onCreateLoader(int id, Bundle args) {
52 | return new CursorLoader(this, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, PROJECTION, null, null, null);
53 | }
54 |
55 | public void onLoadFinished(Loader loader, Cursor cursor) {
56 | adapter.swapCursor(cursor);
57 | Log.i(TAG, cursor.getCount() + " rows");
58 | // String[] names = cursor.getColumnNames();
59 | // int namelen = names.length;
60 | // cursor.moveToFirst();
61 | // while(!cursor.isAfterLast()) {
62 | // StringBuilder bld = new StringBuilder();
63 | // for (int j = 0; j < namelen; ++j) {
64 | // bld.append(names[j]).append("=\"").append(cursor.getString(j)).append("\" ");
65 | // }
66 | // Log.d(TAG, bld.toString());
67 | // cursor.moveToNext();
68 | // }
69 | list.setVisibility(View.VISIBLE);
70 | empty.setVisibility(View.GONE);
71 | }
72 |
73 | public void onLoaderReset(Loader loader) {
74 | adapter.swapCursor(null);
75 | }
76 |
77 | public void onItemClick(AdapterView> parent, View view, int index, long id) {
78 | Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon().appendPath(Long.toString(id)).build();
79 | Intent intent = new Intent(this, ShowImage.class);
80 | intent.setData(uri);
81 | startActivity(intent);
82 | }
83 |
84 | private static class TitleLocationBinder implements SimpleCursorAdapter.ViewBinder {
85 | public boolean setViewValue(View view, Cursor cursor, int i) {
86 | if (i == 1) {
87 | double latitude = cursor.getDouble(i),
88 | longitude = cursor.getDouble(i+1);
89 | if (latitude == 0.0 && longitude == 0.0) {
90 | ((TextView)view).setText(R.string.unknown);
91 | } else {
92 | ((TextView)view).setText(latitude + ", " + longitude);
93 | }
94 | return true;
95 | } else {
96 | return false;
97 | }
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------