summaryrefslogtreecommitdiffstats
path: root/src/com/android/gallery3d/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/gallery3d/util')
-rw-r--r--src/com/android/gallery3d/util/AccessibilityUtils.java54
-rw-r--r--src/com/android/gallery3d/util/BucketNames.java29
-rw-r--r--src/com/android/gallery3d/util/CacheManager.java82
-rw-r--r--src/com/android/gallery3d/util/GalleryUtils.java404
-rw-r--r--src/com/android/gallery3d/util/Holder.java29
-rw-r--r--src/com/android/gallery3d/util/IdentityCache.java78
-rw-r--r--src/com/android/gallery3d/util/IntArray.java60
-rw-r--r--src/com/android/gallery3d/util/InterruptableOutputStream.java67
-rw-r--r--src/com/android/gallery3d/util/JobLimiter.java159
-rw-r--r--src/com/android/gallery3d/util/LinkedNode.java71
-rw-r--r--src/com/android/gallery3d/util/Log.java53
-rw-r--r--src/com/android/gallery3d/util/MediaSetUtils.java66
-rw-r--r--src/com/android/gallery3d/util/MotionEventHelper.java120
-rw-r--r--src/com/android/gallery3d/util/Profile.java226
-rw-r--r--src/com/android/gallery3d/util/ProfileData.java168
-rw-r--r--src/com/android/gallery3d/util/RangeArray.java52
-rw-r--r--src/com/android/gallery3d/util/RangeBoolArray.java49
-rw-r--r--src/com/android/gallery3d/util/RangeIntArray.java49
-rw-r--r--src/com/android/gallery3d/util/ReverseGeocoder.java418
-rw-r--r--src/com/android/gallery3d/util/SaveVideoFileInfo.java29
-rw-r--r--src/com/android/gallery3d/util/SaveVideoFileUtils.java154
-rw-r--r--src/com/android/gallery3d/util/UpdateHelper.java59
22 files changed, 2476 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/util/AccessibilityUtils.java b/src/com/android/gallery3d/util/AccessibilityUtils.java
new file mode 100644
index 000000000..9df8e4ece
--- /dev/null
+++ b/src/com/android/gallery3d/util/AccessibilityUtils.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+import android.content.Context;
+import android.support.v4.view.accessibility.AccessibilityRecordCompat;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.gallery3d.common.ApiHelper;
+
+/**
+ * AccessibilityUtils provides functions needed in accessibility mode. All the functions
+ * in this class are made compatible with gingerbread and later API's
+*/
+public class AccessibilityUtils {
+ public static void makeAnnouncement(View view, CharSequence announcement) {
+ if (view == null)
+ return;
+ if (ApiHelper.HAS_ANNOUNCE_FOR_ACCESSIBILITY) {
+ view.announceForAccessibility(announcement);
+ } else {
+ // For API 15 and earlier, we need to construct an accessibility event
+ Context ctx = view.getContext();
+ AccessibilityManager am = (AccessibilityManager) ctx.getSystemService(
+ Context.ACCESSIBILITY_SERVICE);
+ if (!am.isEnabled()) return;
+ AccessibilityEvent event = AccessibilityEvent.obtain(
+ AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
+ AccessibilityRecordCompat arc = new AccessibilityRecordCompat(event);
+ arc.setSource(view);
+ event.setClassName(view.getClass().getName());
+ event.setPackageName(view.getContext().getPackageName());
+ event.setEnabled(view.isEnabled());
+ event.getText().add(announcement);
+ am.sendAccessibilityEvent(event);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/gallery3d/util/BucketNames.java b/src/com/android/gallery3d/util/BucketNames.java
new file mode 100644
index 000000000..990dc8224
--- /dev/null
+++ b/src/com/android/gallery3d/util/BucketNames.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+/**
+ * Bucket names for buckets that are created and used in the Gallery.
+ */
+public class BucketNames {
+
+ public static final String CAMERA = "DCIM/Camera";
+ public static final String IMPORTED = "Imported";
+ public static final String DOWNLOAD = "download";
+ public static final String EDITED_ONLINE_PHOTOS = "EditedOnlinePhotos";
+ public static final String SCREENSHOTS = "Pictures/Screenshots";
+}
diff --git a/src/com/android/gallery3d/util/CacheManager.java b/src/com/android/gallery3d/util/CacheManager.java
new file mode 100644
index 000000000..ba466f79b
--- /dev/null
+++ b/src/com/android/gallery3d/util/CacheManager.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import com.android.gallery3d.common.BlobCache;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+
+public class CacheManager {
+ private static final String TAG = "CacheManager";
+ private static final String KEY_CACHE_UP_TO_DATE = "cache-up-to-date";
+ private static HashMap<String, BlobCache> sCacheMap =
+ new HashMap<String, BlobCache>();
+ private static boolean sOldCheckDone = false;
+
+ // Return null when we cannot instantiate a BlobCache, e.g.:
+ // there is no SD card found.
+ // This can only be called from data thread.
+ public static BlobCache getCache(Context context, String filename,
+ int maxEntries, int maxBytes, int version) {
+ synchronized (sCacheMap) {
+ if (!sOldCheckDone) {
+ removeOldFilesIfNecessary(context);
+ sOldCheckDone = true;
+ }
+ BlobCache cache = sCacheMap.get(filename);
+ if (cache == null) {
+ File cacheDir = context.getExternalCacheDir();
+ String path = cacheDir.getAbsolutePath() + "/" + filename;
+ try {
+ cache = new BlobCache(path, maxEntries, maxBytes, false,
+ version);
+ sCacheMap.put(filename, cache);
+ } catch (IOException e) {
+ Log.e(TAG, "Cannot instantiate cache!", e);
+ }
+ }
+ return cache;
+ }
+ }
+
+ // Removes the old files if the data is wiped.
+ private static void removeOldFilesIfNecessary(Context context) {
+ SharedPreferences pref = PreferenceManager
+ .getDefaultSharedPreferences(context);
+ int n = 0;
+ try {
+ n = pref.getInt(KEY_CACHE_UP_TO_DATE, 0);
+ } catch (Throwable t) {
+ // ignore.
+ }
+ if (n != 0) return;
+ pref.edit().putInt(KEY_CACHE_UP_TO_DATE, 1).commit();
+
+ File cacheDir = context.getExternalCacheDir();
+ String prefix = cacheDir.getAbsolutePath() + "/";
+
+ BlobCache.deleteFiles(prefix + "imgcache");
+ BlobCache.deleteFiles(prefix + "rev_geocoding");
+ BlobCache.deleteFiles(prefix + "bookmark");
+ }
+}
diff --git a/src/com/android/gallery3d/util/GalleryUtils.java b/src/com/android/gallery3d/util/GalleryUtils.java
new file mode 100644
index 000000000..9245e2c5f
--- /dev/null
+++ b/src/com/android/gallery3d/util/GalleryUtils.java
@@ -0,0 +1,404 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+import android.annotation.TargetApi;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.ConditionVariable;
+import android.os.Environment;
+import android.os.StatFs;
+import android.preference.PreferenceManager;
+import android.provider.MediaStore;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.WindowManager;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.Gallery;
+import com.android.gallery3d.app.PackagesMonitor;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.ui.TiledScreenNail;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+public class GalleryUtils {
+ private static final String TAG = "GalleryUtils";
+ private static final String MAPS_PACKAGE_NAME = "com.google.android.apps.maps";
+ private static final String MAPS_CLASS_NAME = "com.google.android.maps.MapsActivity";
+ private static final String CAMERA_LAUNCHER_NAME = "com.android.camera.CameraLauncher";
+
+ public static final String MIME_TYPE_IMAGE = "image/*";
+ public static final String MIME_TYPE_VIDEO = "video/*";
+ public static final String MIME_TYPE_PANORAMA360 = "application/vnd.google.panorama360+jpg";
+ public static final String MIME_TYPE_ALL = "*/*";
+
+ private static final String DIR_TYPE_IMAGE = "vnd.android.cursor.dir/image";
+ private static final String DIR_TYPE_VIDEO = "vnd.android.cursor.dir/video";
+
+ private static final String PREFIX_PHOTO_EDITOR_UPDATE = "editor-update-";
+ private static final String PREFIX_HAS_PHOTO_EDITOR = "has-editor-";
+
+ private static final String KEY_CAMERA_UPDATE = "camera-update";
+ private static final String KEY_HAS_CAMERA = "has-camera";
+
+ private static float sPixelDensity = -1f;
+ private static boolean sCameraAvailableInitialized = false;
+ private static boolean sCameraAvailable;
+
+ public static void initialize(Context context) {
+ DisplayMetrics metrics = new DisplayMetrics();
+ WindowManager wm = (WindowManager)
+ context.getSystemService(Context.WINDOW_SERVICE);
+ wm.getDefaultDisplay().getMetrics(metrics);
+ sPixelDensity = metrics.density;
+ Resources r = context.getResources();
+ TiledScreenNail.setPlaceholderColor(r.getColor(
+ R.color.bitmap_screennail_placeholder));
+ initializeThumbnailSizes(metrics, r);
+ }
+
+ private static void initializeThumbnailSizes(DisplayMetrics metrics, Resources r) {
+ int maxPixels = Math.max(metrics.heightPixels, metrics.widthPixels);
+
+ // For screen-nails, we never need to completely fill the screen
+ MediaItem.setThumbnailSizes(maxPixels / 2, maxPixels / 5);
+ TiledScreenNail.setMaxSide(maxPixels / 2);
+ }
+
+ public static float[] intColorToFloatARGBArray(int from) {
+ return new float[] {
+ Color.alpha(from) / 255f,
+ Color.red(from) / 255f,
+ Color.green(from) / 255f,
+ Color.blue(from) / 255f
+ };
+ }
+
+ public static float dpToPixel(float dp) {
+ return sPixelDensity * dp;
+ }
+
+ public static int dpToPixel(int dp) {
+ return Math.round(dpToPixel((float) dp));
+ }
+
+ public static int meterToPixel(float meter) {
+ // 1 meter = 39.37 inches, 1 inch = 160 dp.
+ return Math.round(dpToPixel(meter * 39.37f * 160));
+ }
+
+ public static byte[] getBytes(String in) {
+ byte[] result = new byte[in.length() * 2];
+ int output = 0;
+ for (char ch : in.toCharArray()) {
+ result[output++] = (byte) (ch & 0xFF);
+ result[output++] = (byte) (ch >> 8);
+ }
+ return result;
+ }
+
+ // Below are used the detect using database in the render thread. It only
+ // works most of the time, but that's ok because it's for debugging only.
+
+ private static volatile Thread sCurrentThread;
+ private static volatile boolean sWarned;
+
+ public static void setRenderThread() {
+ sCurrentThread = Thread.currentThread();
+ }
+
+ public static void assertNotInRenderThread() {
+ if (!sWarned) {
+ if (Thread.currentThread() == sCurrentThread) {
+ sWarned = true;
+ Log.w(TAG, new Throwable("Should not do this in render thread"));
+ }
+ }
+ }
+
+ private static final double RAD_PER_DEG = Math.PI / 180.0;
+ private static final double EARTH_RADIUS_METERS = 6367000.0;
+
+ public static double fastDistanceMeters(double latRad1, double lngRad1,
+ double latRad2, double lngRad2) {
+ if ((Math.abs(latRad1 - latRad2) > RAD_PER_DEG)
+ || (Math.abs(lngRad1 - lngRad2) > RAD_PER_DEG)) {
+ return accurateDistanceMeters(latRad1, lngRad1, latRad2, lngRad2);
+ }
+ // Approximate sin(x) = x.
+ double sineLat = (latRad1 - latRad2);
+
+ // Approximate sin(x) = x.
+ double sineLng = (lngRad1 - lngRad2);
+
+ // Approximate cos(lat1) * cos(lat2) using
+ // cos((lat1 + lat2)/2) ^ 2
+ double cosTerms = Math.cos((latRad1 + latRad2) / 2.0);
+ cosTerms = cosTerms * cosTerms;
+ double trigTerm = sineLat * sineLat + cosTerms * sineLng * sineLng;
+ trigTerm = Math.sqrt(trigTerm);
+
+ // Approximate arcsin(x) = x
+ return EARTH_RADIUS_METERS * trigTerm;
+ }
+
+ public static double accurateDistanceMeters(double lat1, double lng1,
+ double lat2, double lng2) {
+ double dlat = Math.sin(0.5 * (lat2 - lat1));
+ double dlng = Math.sin(0.5 * (lng2 - lng1));
+ double x = dlat * dlat + dlng * dlng * Math.cos(lat1) * Math.cos(lat2);
+ return (2 * Math.atan2(Math.sqrt(x), Math.sqrt(Math.max(0.0,
+ 1.0 - x)))) * EARTH_RADIUS_METERS;
+ }
+
+
+ public static final double toMile(double meter) {
+ return meter / 1609;
+ }
+
+ // For debugging, it will block the caller for timeout millis.
+ public static void fakeBusy(JobContext jc, int timeout) {
+ final ConditionVariable cv = new ConditionVariable();
+ jc.setCancelListener(new CancelListener() {
+ @Override
+ public void onCancel() {
+ cv.open();
+ }
+ });
+ cv.block(timeout);
+ jc.setCancelListener(null);
+ }
+
+ public static boolean isEditorAvailable(Context context, String mimeType) {
+ int version = PackagesMonitor.getPackagesVersion(context);
+
+ String updateKey = PREFIX_PHOTO_EDITOR_UPDATE + mimeType;
+ String hasKey = PREFIX_HAS_PHOTO_EDITOR + mimeType;
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ if (prefs.getInt(updateKey, 0) != version) {
+ PackageManager packageManager = context.getPackageManager();
+ List<ResolveInfo> infos = packageManager.queryIntentActivities(
+ new Intent(Intent.ACTION_EDIT).setType(mimeType), 0);
+ prefs.edit().putInt(updateKey, version)
+ .putBoolean(hasKey, !infos.isEmpty())
+ .commit();
+ }
+
+ return prefs.getBoolean(hasKey, true);
+ }
+
+ public static boolean isAnyCameraAvailable(Context context) {
+ int version = PackagesMonitor.getPackagesVersion(context);
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ if (prefs.getInt(KEY_CAMERA_UPDATE, 0) != version) {
+ PackageManager packageManager = context.getPackageManager();
+ List<ResolveInfo> infos = packageManager.queryIntentActivities(
+ new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA), 0);
+ prefs.edit().putInt(KEY_CAMERA_UPDATE, version)
+ .putBoolean(KEY_HAS_CAMERA, !infos.isEmpty())
+ .commit();
+ }
+ return prefs.getBoolean(KEY_HAS_CAMERA, true);
+ }
+
+ public static boolean isCameraAvailable(Context context) {
+ if (sCameraAvailableInitialized) return sCameraAvailable;
+ PackageManager pm = context.getPackageManager();
+ ComponentName name = new ComponentName(context, CAMERA_LAUNCHER_NAME);
+ int state = pm.getComponentEnabledSetting(name);
+ sCameraAvailableInitialized = true;
+ sCameraAvailable =
+ (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)
+ || (state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
+ return sCameraAvailable;
+ }
+
+ public static void startCameraActivity(Context context) {
+ Intent intent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA)
+ .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
+ | Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ }
+
+ public static void startGalleryActivity(Context context) {
+ Intent intent = new Intent(context, Gallery.class)
+ .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
+ | Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ }
+
+ public static boolean isValidLocation(double latitude, double longitude) {
+ // TODO: change || to && after we fix the default location issue
+ return (latitude != MediaItem.INVALID_LATLNG || longitude != MediaItem.INVALID_LATLNG);
+ }
+
+ public static String formatLatitudeLongitude(String format, double latitude,
+ double longitude) {
+ // We need to specify the locale otherwise it may go wrong in some language
+ // (e.g. Locale.FRENCH)
+ return String.format(Locale.ENGLISH, format, latitude, longitude);
+ }
+
+ public static void showOnMap(Context context, double latitude, double longitude) {
+ try {
+ // We don't use "geo:latitude,longitude" because it only centers
+ // the MapView to the specified location, but we need a marker
+ // for further operations (routing to/from).
+ // The q=(lat, lng) syntax is suggested by geo-team.
+ String uri = formatLatitudeLongitude("http://maps.google.com/maps?f=q&q=(%f,%f)",
+ latitude, longitude);
+ ComponentName compName = new ComponentName(MAPS_PACKAGE_NAME,
+ MAPS_CLASS_NAME);
+ Intent mapsIntent = new Intent(Intent.ACTION_VIEW,
+ Uri.parse(uri)).setComponent(compName);
+ context.startActivity(mapsIntent);
+ } catch (ActivityNotFoundException e) {
+ // Use the "geo intent" if no GMM is installed
+ Log.e(TAG, "GMM activity not found!", e);
+ String url = formatLatitudeLongitude("geo:%f,%f", latitude, longitude);
+ Intent mapsIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ context.startActivity(mapsIntent);
+ }
+ }
+
+ public static void setViewPointMatrix(
+ float matrix[], float x, float y, float z) {
+ // The matrix is
+ // -z, 0, x, 0
+ // 0, -z, y, 0
+ // 0, 0, 1, 0
+ // 0, 0, 1, -z
+ Arrays.fill(matrix, 0, 16, 0);
+ matrix[0] = matrix[5] = matrix[15] = -z;
+ matrix[8] = x;
+ matrix[9] = y;
+ matrix[10] = matrix[11] = 1;
+ }
+
+ public static int getBucketId(String path) {
+ return path.toLowerCase().hashCode();
+ }
+
+ // Return the local path that matches the given bucketId. If no match is
+ // found, return null
+ public static String searchDirForPath(File dir, int bucketId) {
+ File[] files = dir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ String path = file.getAbsolutePath();
+ if (GalleryUtils.getBucketId(path) == bucketId) {
+ return path;
+ } else {
+ path = searchDirForPath(file, bucketId);
+ if (path != null) return path;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ // Returns a (localized) string for the given duration (in seconds).
+ public static String formatDuration(final Context context, int duration) {
+ int h = duration / 3600;
+ int m = (duration - h * 3600) / 60;
+ int s = duration - (h * 3600 + m * 60);
+ String durationValue;
+ if (h == 0) {
+ durationValue = String.format(context.getString(R.string.details_ms), m, s);
+ } else {
+ durationValue = String.format(context.getString(R.string.details_hms), h, m, s);
+ }
+ return durationValue;
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ public static int determineTypeBits(Context context, Intent intent) {
+ int typeBits = 0;
+ String type = intent.resolveType(context);
+
+ if (MIME_TYPE_ALL.equals(type)) {
+ typeBits = DataManager.INCLUDE_ALL;
+ } else if (MIME_TYPE_IMAGE.equals(type) ||
+ DIR_TYPE_IMAGE.equals(type)) {
+ typeBits = DataManager.INCLUDE_IMAGE;
+ } else if (MIME_TYPE_VIDEO.equals(type) ||
+ DIR_TYPE_VIDEO.equals(type)) {
+ typeBits = DataManager.INCLUDE_VIDEO;
+ } else {
+ typeBits = DataManager.INCLUDE_ALL;
+ }
+
+ if (ApiHelper.HAS_INTENT_EXTRA_LOCAL_ONLY) {
+ if (intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false)) {
+ typeBits |= DataManager.INCLUDE_LOCAL_ONLY;
+ }
+ }
+
+ return typeBits;
+ }
+
+ public static int getSelectionModePrompt(int typeBits) {
+ if ((typeBits & DataManager.INCLUDE_VIDEO) != 0) {
+ return (typeBits & DataManager.INCLUDE_IMAGE) == 0
+ ? R.string.select_video
+ : R.string.select_item;
+ }
+ return R.string.select_image;
+ }
+
+ public static boolean hasSpaceForSize(long size) {
+ String state = Environment.getExternalStorageState();
+ if (!Environment.MEDIA_MOUNTED.equals(state)) {
+ return false;
+ }
+
+ String path = Environment.getExternalStorageDirectory().getPath();
+ try {
+ StatFs stat = new StatFs(path);
+ return stat.getAvailableBlocks() * (long) stat.getBlockSize() > size;
+ } catch (Exception e) {
+ Log.i(TAG, "Fail to access external storage", e);
+ }
+ return false;
+ }
+
+ public static boolean isPanorama(MediaItem item) {
+ if (item == null) return false;
+ int w = item.getWidth();
+ int h = item.getHeight();
+ return (h > 0 && w / h >= 2);
+ }
+}
diff --git a/src/com/android/gallery3d/util/Holder.java b/src/com/android/gallery3d/util/Holder.java
new file mode 100644
index 000000000..0ce914c1d
--- /dev/null
+++ b/src/com/android/gallery3d/util/Holder.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+public class Holder<T> {
+ private T mObject;
+
+ public void set(T object) {
+ mObject = object;
+ }
+
+ public T get() {
+ return mObject;
+ }
+}
diff --git a/src/com/android/gallery3d/util/IdentityCache.java b/src/com/android/gallery3d/util/IdentityCache.java
new file mode 100644
index 000000000..3edc424a3
--- /dev/null
+++ b/src/com/android/gallery3d/util/IdentityCache.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Set;
+
+public class IdentityCache<K, V> {
+
+ private final HashMap<K, Entry<K, V>> mWeakMap =
+ new HashMap<K, Entry<K, V>>();
+ private ReferenceQueue<V> mQueue = new ReferenceQueue<V>();
+
+ public IdentityCache() {
+ }
+
+ private static class Entry<K, V> extends WeakReference<V> {
+ K mKey;
+
+ public Entry(K key, V value, ReferenceQueue<V> queue) {
+ super(value, queue);
+ mKey = key;
+ }
+ }
+
+ private void cleanUpWeakMap() {
+ Entry<K, V> entry = (Entry<K, V>) mQueue.poll();
+ while (entry != null) {
+ mWeakMap.remove(entry.mKey);
+ entry = (Entry<K, V>) mQueue.poll();
+ }
+ }
+
+ public synchronized V put(K key, V value) {
+ cleanUpWeakMap();
+ Entry<K, V> entry = mWeakMap.put(
+ key, new Entry<K, V>(key, value, mQueue));
+ return entry == null ? null : entry.get();
+ }
+
+ public synchronized V get(K key) {
+ cleanUpWeakMap();
+ Entry<K, V> entry = mWeakMap.get(key);
+ return entry == null ? null : entry.get();
+ }
+
+ // This is currently unused.
+ /*
+ public synchronized void clear() {
+ mWeakMap.clear();
+ mQueue = new ReferenceQueue<V>();
+ }
+ */
+
+ // This is for debugging only
+ public synchronized ArrayList<K> keys() {
+ Set<K> set = mWeakMap.keySet();
+ ArrayList<K> result = new ArrayList<K>(set);
+ return result;
+ }
+}
diff --git a/src/com/android/gallery3d/util/IntArray.java b/src/com/android/gallery3d/util/IntArray.java
new file mode 100644
index 000000000..2c4dc2c83
--- /dev/null
+++ b/src/com/android/gallery3d/util/IntArray.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+public class IntArray {
+ private static final int INIT_CAPACITY = 8;
+
+ private int mData[] = new int[INIT_CAPACITY];
+ private int mSize = 0;
+
+ public void add(int value) {
+ if (mData.length == mSize) {
+ int temp[] = new int[mSize + mSize];
+ System.arraycopy(mData, 0, temp, 0, mSize);
+ mData = temp;
+ }
+ mData[mSize++] = value;
+ }
+
+ public int removeLast() {
+ mSize--;
+ return mData[mSize];
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ // For testing only
+ public int[] toArray(int[] result) {
+ if (result == null || result.length < mSize) {
+ result = new int[mSize];
+ }
+ System.arraycopy(mData, 0, result, 0, mSize);
+ return result;
+ }
+
+ public int[] getInternalArray() {
+ return mData;
+ }
+
+ public void clear() {
+ mSize = 0;
+ if (mData.length != INIT_CAPACITY) mData = new int[INIT_CAPACITY];
+ }
+}
diff --git a/src/com/android/gallery3d/util/InterruptableOutputStream.java b/src/com/android/gallery3d/util/InterruptableOutputStream.java
new file mode 100644
index 000000000..1ab62ab98
--- /dev/null
+++ b/src/com/android/gallery3d/util/InterruptableOutputStream.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+import com.android.gallery3d.common.Utils;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+
+public class InterruptableOutputStream extends OutputStream {
+
+ private static final int MAX_WRITE_BYTES = 4096;
+
+ private OutputStream mOutputStream;
+ private volatile boolean mIsInterrupted = false;
+
+ public InterruptableOutputStream(OutputStream outputStream) {
+ mOutputStream = Utils.checkNotNull(outputStream);
+ }
+
+ @Override
+ public void write(int oneByte) throws IOException {
+ if (mIsInterrupted) throw new InterruptedIOException();
+ mOutputStream.write(oneByte);
+ }
+
+ @Override
+ public void write(byte[] buffer, int offset, int count) throws IOException {
+ int end = offset + count;
+ while (offset < end) {
+ if (mIsInterrupted) throw new InterruptedIOException();
+ int bytesCount = Math.min(MAX_WRITE_BYTES, end - offset);
+ mOutputStream.write(buffer, offset, bytesCount);
+ offset += bytesCount;
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ mOutputStream.close();
+ }
+
+ @Override
+ public void flush() throws IOException {
+ if (mIsInterrupted) throw new InterruptedIOException();
+ mOutputStream.flush();
+ }
+
+ public void interrupt() {
+ mIsInterrupted = true;
+ }
+}
diff --git a/src/com/android/gallery3d/util/JobLimiter.java b/src/com/android/gallery3d/util/JobLimiter.java
new file mode 100644
index 000000000..42b754153
--- /dev/null
+++ b/src/com/android/gallery3d/util/JobLimiter.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.LinkedList;
+
+// Limit the number of concurrent jobs that has been submitted into a ThreadPool
+@SuppressWarnings("rawtypes")
+public class JobLimiter implements FutureListener {
+ private static final String TAG = "JobLimiter";
+
+ // State Transition:
+ // INIT -> DONE, CANCELLED
+ // DONE -> CANCELLED
+ private static final int STATE_INIT = 0;
+ private static final int STATE_DONE = 1;
+ private static final int STATE_CANCELLED = 2;
+
+ private final LinkedList<JobWrapper<?>> mJobs = new LinkedList<JobWrapper<?>>();
+ private final ThreadPool mPool;
+ private int mLimit;
+
+ private static class JobWrapper<T> implements Future<T>, Job<T> {
+ private int mState = STATE_INIT;
+ private Job<T> mJob;
+ private Future<T> mDelegate;
+ private FutureListener<T> mListener;
+ private T mResult;
+
+ public JobWrapper(Job<T> job, FutureListener<T> listener) {
+ mJob = job;
+ mListener = listener;
+ }
+
+ public synchronized void setFuture(Future<T> future) {
+ if (mState != STATE_INIT) return;
+ mDelegate = future;
+ }
+
+ @Override
+ public void cancel() {
+ FutureListener<T> listener = null;
+ synchronized (this) {
+ if (mState != STATE_DONE) {
+ listener = mListener;
+ mJob = null;
+ mListener = null;
+ if (mDelegate != null) {
+ mDelegate.cancel();
+ mDelegate = null;
+ }
+ }
+ mState = STATE_CANCELLED;
+ mResult = null;
+ notifyAll();
+ }
+ if (listener != null) listener.onFutureDone(this);
+ }
+
+ @Override
+ public synchronized boolean isCancelled() {
+ return mState == STATE_CANCELLED;
+ }
+
+ @Override
+ public boolean isDone() {
+ // Both CANCELLED AND DONE is considered as done
+ return mState != STATE_INIT;
+ }
+
+ @Override
+ public synchronized T get() {
+ while (mState == STATE_INIT) {
+ // handle the interrupted exception of wait()
+ Utils.waitWithoutInterrupt(this);
+ }
+ return mResult;
+ }
+
+ @Override
+ public void waitDone() {
+ get();
+ }
+
+ @Override
+ public T run(JobContext jc) {
+ Job<T> job = null;
+ synchronized (this) {
+ if (mState == STATE_CANCELLED) return null;
+ job = mJob;
+ }
+ T result = null;
+ try {
+ result = job.run(jc);
+ } catch (Throwable t) {
+ Log.w(TAG, "error executing job: " + job, t);
+ }
+ FutureListener<T> listener = null;
+ synchronized (this) {
+ if (mState == STATE_CANCELLED) return null;
+ mState = STATE_DONE;
+ listener = mListener;
+ mListener = null;
+ mJob = null;
+ mResult = result;
+ notifyAll();
+ }
+ if (listener != null) listener.onFutureDone(this);
+ return result;
+ }
+ }
+
+ public JobLimiter(ThreadPool pool, int limit) {
+ mPool = Utils.checkNotNull(pool);
+ mLimit = limit;
+ }
+
+ public synchronized <T> Future<T> submit(Job<T> job, FutureListener<T> listener) {
+ JobWrapper<T> future = new JobWrapper<T>(Utils.checkNotNull(job), listener);
+ mJobs.addLast(future);
+ submitTasksIfAllowed();
+ return future;
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private void submitTasksIfAllowed() {
+ while (mLimit > 0 && !mJobs.isEmpty()) {
+ JobWrapper wrapper = mJobs.removeFirst();
+ if (!wrapper.isCancelled()) {
+ --mLimit;
+ wrapper.setFuture(mPool.submit(wrapper, this));
+ }
+ }
+ }
+
+ @Override
+ public synchronized void onFutureDone(Future future) {
+ ++mLimit;
+ submitTasksIfAllowed();
+ }
+}
diff --git a/src/com/android/gallery3d/util/LinkedNode.java b/src/com/android/gallery3d/util/LinkedNode.java
new file mode 100644
index 000000000..4cfc3cded
--- /dev/null
+++ b/src/com/android/gallery3d/util/LinkedNode.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+
+public class LinkedNode {
+ private LinkedNode mPrev;
+ private LinkedNode mNext;
+
+ public LinkedNode() {
+ mPrev = mNext = this;
+ }
+
+ public void insert(LinkedNode node) {
+ node.mNext = mNext;
+ mNext.mPrev = node;
+ node.mPrev = this;
+ mNext = node;
+ }
+
+ public void remove() {
+ if (mNext == this) throw new IllegalStateException();
+ mPrev.mNext = mNext;
+ mNext.mPrev = mPrev;
+ mPrev = mNext = null;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static class List<T extends LinkedNode> {
+ private LinkedNode mHead = new LinkedNode();
+
+ public void insertLast(T node) {
+ mHead.mPrev.insert(node);
+ }
+
+ public T getFirst() {
+ return (T) (mHead.mNext == mHead ? null : mHead.mNext);
+ }
+
+ public T getLast() {
+ return (T) (mHead.mPrev == mHead ? null : mHead.mPrev);
+ }
+
+ public T nextOf(T node) {
+ return (T) (node.mNext == mHead ? null : node.mNext);
+ }
+
+ public T previousOf(T node) {
+ return (T) (node.mPrev == mHead ? null : node.mPrev);
+ }
+
+ }
+
+ public static <T extends LinkedNode> List<T> newList() {
+ return new List<T>();
+ }
+}
diff --git a/src/com/android/gallery3d/util/Log.java b/src/com/android/gallery3d/util/Log.java
new file mode 100644
index 000000000..d7f8e85d0
--- /dev/null
+++ b/src/com/android/gallery3d/util/Log.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+public class Log {
+ public static int v(String tag, String msg) {
+ return android.util.Log.v(tag, msg);
+ }
+ public static int v(String tag, String msg, Throwable tr) {
+ return android.util.Log.v(tag, msg, tr);
+ }
+ public static int d(String tag, String msg) {
+ return android.util.Log.d(tag, msg);
+ }
+ public static int d(String tag, String msg, Throwable tr) {
+ return android.util.Log.d(tag, msg, tr);
+ }
+ public static int i(String tag, String msg) {
+ return android.util.Log.i(tag, msg);
+ }
+ public static int i(String tag, String msg, Throwable tr) {
+ return android.util.Log.i(tag, msg, tr);
+ }
+ public static int w(String tag, String msg) {
+ return android.util.Log.w(tag, msg);
+ }
+ public static int w(String tag, String msg, Throwable tr) {
+ return android.util.Log.w(tag, msg, tr);
+ }
+ public static int w(String tag, Throwable tr) {
+ return android.util.Log.w(tag, tr);
+ }
+ public static int e(String tag, String msg) {
+ return android.util.Log.e(tag, msg);
+ }
+ public static int e(String tag, String msg, Throwable tr) {
+ return android.util.Log.e(tag, msg, tr);
+ }
+}
diff --git a/src/com/android/gallery3d/util/MediaSetUtils.java b/src/com/android/gallery3d/util/MediaSetUtils.java
new file mode 100644
index 000000000..043800561
--- /dev/null
+++ b/src/com/android/gallery3d/util/MediaSetUtils.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+import android.os.Environment;
+
+import com.android.gallery3d.data.LocalAlbum;
+import com.android.gallery3d.data.LocalMergeAlbum;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+
+import java.util.Comparator;
+
+public class MediaSetUtils {
+ public static final Comparator<MediaSet> NAME_COMPARATOR = new NameComparator();
+
+ public static final int CAMERA_BUCKET_ID = GalleryUtils.getBucketId(
+ Environment.getExternalStorageDirectory().toString() + "/"
+ + BucketNames.CAMERA);
+ public static final int DOWNLOAD_BUCKET_ID = GalleryUtils.getBucketId(
+ Environment.getExternalStorageDirectory().toString() + "/"
+ + BucketNames.DOWNLOAD);
+ public static final int EDITED_ONLINE_PHOTOS_BUCKET_ID = GalleryUtils.getBucketId(
+ Environment.getExternalStorageDirectory().toString() + "/"
+ + BucketNames.EDITED_ONLINE_PHOTOS);
+ public static final int IMPORTED_BUCKET_ID = GalleryUtils.getBucketId(
+ Environment.getExternalStorageDirectory().toString() + "/"
+ + BucketNames.IMPORTED);
+ public static final int SNAPSHOT_BUCKET_ID = GalleryUtils.getBucketId(
+ Environment.getExternalStorageDirectory().toString() +
+ "/" + BucketNames.SCREENSHOTS);
+
+ private static final Path[] CAMERA_PATHS = {
+ Path.fromString("/local/all/" + CAMERA_BUCKET_ID),
+ Path.fromString("/local/image/" + CAMERA_BUCKET_ID),
+ Path.fromString("/local/video/" + CAMERA_BUCKET_ID)};
+
+ public static boolean isCameraSource(Path path) {
+ return CAMERA_PATHS[0] == path || CAMERA_PATHS[1] == path
+ || CAMERA_PATHS[2] == path;
+ }
+
+ // Sort MediaSets by name
+ public static class NameComparator implements Comparator<MediaSet> {
+ @Override
+ public int compare(MediaSet set1, MediaSet set2) {
+ int result = set1.getName().compareToIgnoreCase(set2.getName());
+ if (result != 0) return result;
+ return set1.getPath().toString().compareTo(set2.getPath().toString());
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/util/MotionEventHelper.java b/src/com/android/gallery3d/util/MotionEventHelper.java
new file mode 100644
index 000000000..715f7fa69
--- /dev/null
+++ b/src/com/android/gallery3d/util/MotionEventHelper.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+import android.annotation.TargetApi;
+import android.graphics.Matrix;
+import android.util.FloatMath;
+import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+
+import com.android.gallery3d.common.ApiHelper;
+
+public final class MotionEventHelper {
+ private MotionEventHelper() {}
+
+ public static MotionEvent transformEvent(MotionEvent e, Matrix m) {
+ // We try to use the new transform method if possible because it uses
+ // less memory.
+ if (ApiHelper.HAS_MOTION_EVENT_TRANSFORM) {
+ return transformEventNew(e, m);
+ } else {
+ return transformEventOld(e, m);
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ private static MotionEvent transformEventNew(MotionEvent e, Matrix m) {
+ MotionEvent newEvent = MotionEvent.obtain(e);
+ newEvent.transform(m);
+ return newEvent;
+ }
+
+ // This is copied from Input.cpp in the android framework.
+ private static MotionEvent transformEventOld(MotionEvent e, Matrix m) {
+ long downTime = e.getDownTime();
+ long eventTime = e.getEventTime();
+ int action = e.getAction();
+ int pointerCount = e.getPointerCount();
+ int[] pointerIds = getPointerIds(e);
+ PointerCoords[] pointerCoords = getPointerCoords(e);
+ int metaState = e.getMetaState();
+ float xPrecision = e.getXPrecision();
+ float yPrecision = e.getYPrecision();
+ int deviceId = e.getDeviceId();
+ int edgeFlags = e.getEdgeFlags();
+ int source = e.getSource();
+ int flags = e.getFlags();
+
+ // Copy the x and y coordinates into an array, map them, and copy back.
+ float[] xy = new float[pointerCoords.length * 2];
+ for (int i = 0; i < pointerCount;i++) {
+ xy[2 * i] = pointerCoords[i].x;
+ xy[2 * i + 1] = pointerCoords[i].y;
+ }
+ m.mapPoints(xy);
+ for (int i = 0; i < pointerCount;i++) {
+ pointerCoords[i].x = xy[2 * i];
+ pointerCoords[i].y = xy[2 * i + 1];
+ pointerCoords[i].orientation = transformAngle(
+ m, pointerCoords[i].orientation);
+ }
+
+ MotionEvent n = MotionEvent.obtain(downTime, eventTime, action,
+ pointerCount, pointerIds, pointerCoords, metaState, xPrecision,
+ yPrecision, deviceId, edgeFlags, source, flags);
+
+ return n;
+ }
+
+ private static int[] getPointerIds(MotionEvent e) {
+ int n = e.getPointerCount();
+ int[] r = new int[n];
+ for (int i = 0; i < n; i++) {
+ r[i] = e.getPointerId(i);
+ }
+ return r;
+ }
+
+ private static PointerCoords[] getPointerCoords(MotionEvent e) {
+ int n = e.getPointerCount();
+ PointerCoords[] r = new PointerCoords[n];
+ for (int i = 0; i < n; i++) {
+ r[i] = new PointerCoords();
+ e.getPointerCoords(i, r[i]);
+ }
+ return r;
+ }
+
+ private static float transformAngle(Matrix m, float angleRadians) {
+ // Construct and transform a vector oriented at the specified clockwise
+ // angle from vertical. Coordinate system: down is increasing Y, right is
+ // increasing X.
+ float[] v = new float[2];
+ v[0] = FloatMath.sin(angleRadians);
+ v[1] = -FloatMath.cos(angleRadians);
+ m.mapVectors(v);
+
+ // Derive the transformed vector's clockwise angle from vertical.
+ float result = (float) Math.atan2(v[0], -v[1]);
+ if (result < -Math.PI / 2) {
+ result += Math.PI;
+ } else if (result > Math.PI / 2) {
+ result -= Math.PI;
+ }
+ return result;
+ }
+}
diff --git a/src/com/android/gallery3d/util/Profile.java b/src/com/android/gallery3d/util/Profile.java
new file mode 100644
index 000000000..7ed72c90e
--- /dev/null
+++ b/src/com/android/gallery3d/util/Profile.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+
+import java.util.ArrayList;
+import java.util.Random;
+
+// The Profile class is used to collect profiling information for a thread. It
+// samples stack traces for a thread periodically. enable() and disable() is
+// used to enable and disable profiling for the calling thread. The profiling
+// information can then be dumped to a file using the dumpToFile() method.
+//
+// The disableAll() method can be used to disable profiling for all threads and
+// can be called in onPause() to ensure all profiling is disabled when an
+// activity is paused.
+public class Profile {
+ @SuppressWarnings("unused")
+ private static final String TAG = "Profile";
+ private static final int NS_PER_MS = 1000000;
+
+ // This is a watchdog entry for one thread.
+ // For every cycleTime period, we dump the stack of the thread.
+ private static class WatchEntry {
+ Thread thread;
+
+ // Both are in milliseconds
+ int cycleTime;
+ int wakeTime;
+
+ boolean isHolding;
+ ArrayList<String[]> holdingStacks = new ArrayList<String[]>();
+ }
+
+ // This is a watchdog thread which dumps stacks of other threads periodically.
+ private static Watchdog sWatchdog = new Watchdog();
+
+ private static class Watchdog {
+ private ArrayList<WatchEntry> mList = new ArrayList<WatchEntry>();
+ private HandlerThread mHandlerThread;
+ private Handler mHandler;
+ private Runnable mProcessRunnable = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (Watchdog.this) {
+ processList();
+ }
+ }
+ };
+ private Random mRandom = new Random();
+ private ProfileData mProfileData = new ProfileData();
+
+ public Watchdog() {
+ mHandlerThread = new HandlerThread("Watchdog Handler",
+ Process.THREAD_PRIORITY_FOREGROUND);
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+ }
+
+ public synchronized void addWatchEntry(Thread thread, int cycleTime) {
+ WatchEntry e = new WatchEntry();
+ e.thread = thread;
+ e.cycleTime = cycleTime;
+ int firstDelay = 1 + mRandom.nextInt(cycleTime);
+ e.wakeTime = (int) (System.nanoTime() / NS_PER_MS) + firstDelay;
+ mList.add(e);
+ processList();
+ }
+
+ public synchronized void removeWatchEntry(Thread thread) {
+ for (int i = 0; i < mList.size(); i++) {
+ if (mList.get(i).thread == thread) {
+ mList.remove(i);
+ break;
+ }
+ }
+ processList();
+ }
+
+ public synchronized void removeAllWatchEntries() {
+ mList.clear();
+ processList();
+ }
+
+ private void processList() {
+ mHandler.removeCallbacks(mProcessRunnable);
+ if (mList.size() == 0) return;
+
+ int currentTime = (int) (System.nanoTime() / NS_PER_MS);
+ int nextWakeTime = 0;
+
+ for (WatchEntry entry : mList) {
+ if (currentTime > entry.wakeTime) {
+ entry.wakeTime += entry.cycleTime;
+ Thread thread = entry.thread;
+ sampleStack(entry);
+ }
+
+ if (entry.wakeTime > nextWakeTime) {
+ nextWakeTime = entry.wakeTime;
+ }
+ }
+
+ long delay = nextWakeTime - currentTime;
+ mHandler.postDelayed(mProcessRunnable, delay);
+ }
+
+ private void sampleStack(WatchEntry entry) {
+ Thread thread = entry.thread;
+ StackTraceElement[] stack = thread.getStackTrace();
+ String[] lines = new String[stack.length];
+ for (int i = 0; i < stack.length; i++) {
+ lines[i] = stack[i].toString();
+ }
+ if (entry.isHolding) {
+ entry.holdingStacks.add(lines);
+ } else {
+ mProfileData.addSample(lines);
+ }
+ }
+
+ private WatchEntry findEntry(Thread thread) {
+ for (int i = 0; i < mList.size(); i++) {
+ WatchEntry entry = mList.get(i);
+ if (entry.thread == thread) return entry;
+ }
+ return null;
+ }
+
+ public synchronized void dumpToFile(String filename) {
+ mProfileData.dumpToFile(filename);
+ }
+
+ public synchronized void reset() {
+ mProfileData.reset();
+ }
+
+ public synchronized void hold(Thread t) {
+ WatchEntry entry = findEntry(t);
+
+ // This can happen if the profiling is disabled (probably from
+ // another thread). Same check is applied in commit() and drop()
+ // below.
+ if (entry == null) return;
+
+ entry.isHolding = true;
+ }
+
+ public synchronized void commit(Thread t) {
+ WatchEntry entry = findEntry(t);
+ if (entry == null) return;
+ ArrayList<String[]> stacks = entry.holdingStacks;
+ for (int i = 0; i < stacks.size(); i++) {
+ mProfileData.addSample(stacks.get(i));
+ }
+ entry.isHolding = false;
+ entry.holdingStacks.clear();
+ }
+
+ public synchronized void drop(Thread t) {
+ WatchEntry entry = findEntry(t);
+ if (entry == null) return;
+ entry.isHolding = false;
+ entry.holdingStacks.clear();
+ }
+ }
+
+ // Enable profiling for the calling thread. Periodically (every cycleTimeInMs
+ // milliseconds) sample the stack trace of the calling thread.
+ public static void enable(int cycleTimeInMs) {
+ Thread t = Thread.currentThread();
+ sWatchdog.addWatchEntry(t, cycleTimeInMs);
+ }
+
+ // Disable profiling for the calling thread.
+ public static void disable() {
+ sWatchdog.removeWatchEntry(Thread.currentThread());
+ }
+
+ // Disable profiling for all threads.
+ public static void disableAll() {
+ sWatchdog.removeAllWatchEntries();
+ }
+
+ // Dump the profiling data to a file.
+ public static void dumpToFile(String filename) {
+ sWatchdog.dumpToFile(filename);
+ }
+
+ // Reset the collected profiling data.
+ public static void reset() {
+ sWatchdog.reset();
+ }
+
+ // Hold the future samples coming from current thread until commit() or
+ // drop() is called, and those samples are recorded or ignored as a result.
+ // This must called after enable() to be effective.
+ public static void hold() {
+ sWatchdog.hold(Thread.currentThread());
+ }
+
+ public static void commit() {
+ sWatchdog.commit(Thread.currentThread());
+ }
+
+ public static void drop() {
+ sWatchdog.drop(Thread.currentThread());
+ }
+}
diff --git a/src/com/android/gallery3d/util/ProfileData.java b/src/com/android/gallery3d/util/ProfileData.java
new file mode 100644
index 000000000..a1bb8e1e4
--- /dev/null
+++ b/src/com/android/gallery3d/util/ProfileData.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+import android.util.Log;
+
+import com.android.gallery3d.common.Utils;
+
+import java.io.DataOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map.Entry;
+
+// ProfileData keeps profiling samples in a tree structure.
+// The addSample() method adds a sample. The dumpToFile() method saves the data
+// to a file. The reset() method clears all samples.
+public class ProfileData {
+ @SuppressWarnings("unused")
+ private static final String TAG = "ProfileData";
+
+ private static class Node {
+ public int id; // this is the name of this node, mapped from mNameToId
+ public Node parent;
+ public int sampleCount;
+ public ArrayList<Node> children;
+ public Node(Node parent, int id) {
+ this.parent = parent;
+ this.id = id;
+ }
+ }
+
+ private Node mRoot;
+ private int mNextId;
+ private HashMap<String, Integer> mNameToId;
+ private DataOutputStream mOut;
+ private byte mScratch[] = new byte[4]; // scratch space for writeInt()
+
+ public ProfileData() {
+ mRoot = new Node(null, -1); // The id of the root node is unused.
+ mNameToId = new HashMap<String, Integer>();
+ }
+
+ public void reset() {
+ mRoot = new Node(null, -1);
+ mNameToId.clear();
+ mNextId = 0;
+ }
+
+ private int nameToId(String name) {
+ Integer id = mNameToId.get(name);
+ if (id == null) {
+ id = ++mNextId; // The tool doesn't want id=0, so we start from 1.
+ mNameToId.put(name, id);
+ }
+ return id;
+ }
+
+ public void addSample(String[] stack) {
+ int[] ids = new int[stack.length];
+ for (int i = 0; i < stack.length; i++) {
+ ids[i] = nameToId(stack[i]);
+ }
+
+ Node node = mRoot;
+ for (int i = stack.length - 1; i >= 0; i--) {
+ if (node.children == null) {
+ node.children = new ArrayList<Node>();
+ }
+
+ int id = ids[i];
+ ArrayList<Node> children = node.children;
+ int j;
+ for (j = 0; j < children.size(); j++) {
+ if (children.get(j).id == id) break;
+ }
+ if (j == children.size()) {
+ children.add(new Node(node, id));
+ }
+
+ node = children.get(j);
+ }
+
+ node.sampleCount++;
+ }
+
+ public void dumpToFile(String filename) {
+ try {
+ mOut = new DataOutputStream(new FileOutputStream(filename));
+ // Start record
+ writeInt(0);
+ writeInt(3);
+ writeInt(1);
+ writeInt(20000); // Sampling period: 20ms
+ writeInt(0);
+
+ // Samples
+ writeAllStacks(mRoot, 0);
+
+ // End record
+ writeInt(0);
+ writeInt(1);
+ writeInt(0);
+ writeAllSymbols();
+ } catch (IOException ex) {
+ Log.w("Failed to dump to file", ex);
+ } finally {
+ Utils.closeSilently(mOut);
+ }
+ }
+
+ // Writes out one stack, consisting of N+2 words:
+ // first word: sample count
+ // second word: depth of the stack (N)
+ // N words: each word is the id of one address in the stack
+ private void writeOneStack(Node node, int depth) throws IOException {
+ writeInt(node.sampleCount);
+ writeInt(depth);
+ while (depth-- > 0) {
+ writeInt(node.id);
+ node = node.parent;
+ }
+ }
+
+ private void writeAllStacks(Node node, int depth) throws IOException {
+ if (node.sampleCount > 0) {
+ writeOneStack(node, depth);
+ }
+
+ ArrayList<Node> children = node.children;
+ if (children != null) {
+ for (int i = 0; i < children.size(); i++) {
+ writeAllStacks(children.get(i), depth + 1);
+ }
+ }
+ }
+
+ // Writes out the symbol table. Each line is like:
+ // 0x17e java.util.ArrayList.isEmpty(ArrayList.java:319)
+ private void writeAllSymbols() throws IOException {
+ for (Entry<String, Integer> entry : mNameToId.entrySet()) {
+ mOut.writeBytes(String.format("0x%x %s\n", entry.getValue(), entry.getKey()));
+ }
+ }
+
+ private void writeInt(int v) throws IOException {
+ mScratch[0] = (byte) v;
+ mScratch[1] = (byte) (v >> 8);
+ mScratch[2] = (byte) (v >> 16);
+ mScratch[3] = (byte) (v >> 24);
+ mOut.write(mScratch);
+ }
+}
diff --git a/src/com/android/gallery3d/util/RangeArray.java b/src/com/android/gallery3d/util/RangeArray.java
new file mode 100644
index 000000000..8e61348a3
--- /dev/null
+++ b/src/com/android/gallery3d/util/RangeArray.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+// This is an array whose index ranges from min to max (inclusive).
+public class RangeArray<T> {
+ private T[] mData;
+ private int mOffset;
+
+ public RangeArray(int min, int max) {
+ mData = (T[]) new Object[max - min + 1];
+ mOffset = min;
+ }
+
+ // Wraps around an existing array
+ public RangeArray(T[] src, int min, int max) {
+ if (max - min + 1 != src.length) {
+ throw new AssertionError();
+ }
+ mData = src;
+ mOffset = min;
+ }
+
+ public void put(int i, T object) {
+ mData[i - mOffset] = object;
+ }
+
+ public T get(int i) {
+ return mData[i - mOffset];
+ }
+
+ public int indexOf(T object) {
+ for (int i = 0; i < mData.length; i++) {
+ if (mData[i] == object) return i + mOffset;
+ }
+ return Integer.MAX_VALUE;
+ }
+}
diff --git a/src/com/android/gallery3d/util/RangeBoolArray.java b/src/com/android/gallery3d/util/RangeBoolArray.java
new file mode 100644
index 000000000..035fc40a4
--- /dev/null
+++ b/src/com/android/gallery3d/util/RangeBoolArray.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+// This is an array whose index ranges from min to max (inclusive).
+public class RangeBoolArray {
+ private boolean[] mData;
+ private int mOffset;
+
+ public RangeBoolArray(int min, int max) {
+ mData = new boolean[max - min + 1];
+ mOffset = min;
+ }
+
+ // Wraps around an existing array
+ public RangeBoolArray(boolean[] src, int min, int max) {
+ mData = src;
+ mOffset = min;
+ }
+
+ public void put(int i, boolean object) {
+ mData[i - mOffset] = object;
+ }
+
+ public boolean get(int i) {
+ return mData[i - mOffset];
+ }
+
+ public int indexOf(boolean object) {
+ for (int i = 0; i < mData.length; i++) {
+ if (mData[i] == object) return i + mOffset;
+ }
+ return Integer.MAX_VALUE;
+ }
+}
diff --git a/src/com/android/gallery3d/util/RangeIntArray.java b/src/com/android/gallery3d/util/RangeIntArray.java
new file mode 100644
index 000000000..9dbb99fac
--- /dev/null
+++ b/src/com/android/gallery3d/util/RangeIntArray.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+// This is an array whose index ranges from min to max (inclusive).
+public class RangeIntArray {
+ private int[] mData;
+ private int mOffset;
+
+ public RangeIntArray(int min, int max) {
+ mData = new int[max - min + 1];
+ mOffset = min;
+ }
+
+ // Wraps around an existing array
+ public RangeIntArray(int[] src, int min, int max) {
+ mData = src;
+ mOffset = min;
+ }
+
+ public void put(int i, int object) {
+ mData[i - mOffset] = object;
+ }
+
+ public int get(int i) {
+ return mData[i - mOffset];
+ }
+
+ public int indexOf(int object) {
+ for (int i = 0; i < mData.length; i++) {
+ if (mData[i] == object) return i + mOffset;
+ }
+ return Integer.MAX_VALUE;
+ }
+}
diff --git a/src/com/android/gallery3d/util/ReverseGeocoder.java b/src/com/android/gallery3d/util/ReverseGeocoder.java
new file mode 100644
index 000000000..a8b26d9b5
--- /dev/null
+++ b/src/com/android/gallery3d/util/ReverseGeocoder.java
@@ -0,0 +1,418 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+import android.content.Context;
+import android.location.Address;
+import android.location.Geocoder;
+import android.location.Location;
+import android.location.LocationManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+
+import com.android.gallery3d.common.BlobCache;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+
+public class ReverseGeocoder {
+ @SuppressWarnings("unused")
+ private static final String TAG = "ReverseGeocoder";
+ public static final int EARTH_RADIUS_METERS = 6378137;
+ public static final int LAT_MIN = -90;
+ public static final int LAT_MAX = 90;
+ public static final int LON_MIN = -180;
+ public static final int LON_MAX = 180;
+ private static final int MAX_COUNTRY_NAME_LENGTH = 8;
+ // If two points are within 20 miles of each other, use
+ // "Around Palo Alto, CA" or "Around Mountain View, CA".
+ // instead of directly jumping to the next level and saying
+ // "California, US".
+ private static final int MAX_LOCALITY_MILE_RANGE = 20;
+
+ private static final String GEO_CACHE_FILE = "rev_geocoding";
+ private static final int GEO_CACHE_MAX_ENTRIES = 1000;
+ private static final int GEO_CACHE_MAX_BYTES = 500 * 1024;
+ private static final int GEO_CACHE_VERSION = 0;
+
+ public static class SetLatLong {
+ // The latitude and longitude of the min latitude point.
+ public double mMinLatLatitude = LAT_MAX;
+ public double mMinLatLongitude;
+ // The latitude and longitude of the max latitude point.
+ public double mMaxLatLatitude = LAT_MIN;
+ public double mMaxLatLongitude;
+ // The latitude and longitude of the min longitude point.
+ public double mMinLonLatitude;
+ public double mMinLonLongitude = LON_MAX;
+ // The latitude and longitude of the max longitude point.
+ public double mMaxLonLatitude;
+ public double mMaxLonLongitude = LON_MIN;
+ }
+
+ private Context mContext;
+ private Geocoder mGeocoder;
+ private BlobCache mGeoCache;
+ private ConnectivityManager mConnectivityManager;
+ private static Address sCurrentAddress; // last known address
+
+ public ReverseGeocoder(Context context) {
+ mContext = context;
+ mGeocoder = new Geocoder(mContext);
+ mGeoCache = CacheManager.getCache(context, GEO_CACHE_FILE,
+ GEO_CACHE_MAX_ENTRIES, GEO_CACHE_MAX_BYTES,
+ GEO_CACHE_VERSION);
+ mConnectivityManager = (ConnectivityManager)
+ context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ }
+
+ public String computeAddress(SetLatLong set) {
+ // The overall min and max latitudes and longitudes of the set.
+ double setMinLatitude = set.mMinLatLatitude;
+ double setMinLongitude = set.mMinLatLongitude;
+ double setMaxLatitude = set.mMaxLatLatitude;
+ double setMaxLongitude = set.mMaxLatLongitude;
+ if (Math.abs(set.mMaxLatLatitude - set.mMinLatLatitude)
+ < Math.abs(set.mMaxLonLongitude - set.mMinLonLongitude)) {
+ setMinLatitude = set.mMinLonLatitude;
+ setMinLongitude = set.mMinLonLongitude;
+ setMaxLatitude = set.mMaxLonLatitude;
+ setMaxLongitude = set.mMaxLonLongitude;
+ }
+ Address addr1 = lookupAddress(setMinLatitude, setMinLongitude, true);
+ Address addr2 = lookupAddress(setMaxLatitude, setMaxLongitude, true);
+ if (addr1 == null)
+ addr1 = addr2;
+ if (addr2 == null)
+ addr2 = addr1;
+ if (addr1 == null || addr2 == null) {
+ return null;
+ }
+
+ // Get current location, we decide the granularity of the string based
+ // on this.
+ LocationManager locationManager =
+ (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
+ Location location = null;
+ List<String> providers = locationManager.getAllProviders();
+ for (int i = 0; i < providers.size(); ++i) {
+ String provider = providers.get(i);
+ location = (provider != null) ? locationManager.getLastKnownLocation(provider) : null;
+ if (location != null)
+ break;
+ }
+ String currentCity = "";
+ String currentAdminArea = "";
+ String currentCountry = Locale.getDefault().getCountry();
+ if (location != null) {
+ Address currentAddress = lookupAddress(
+ location.getLatitude(), location.getLongitude(), true);
+ if (currentAddress == null) {
+ currentAddress = sCurrentAddress;
+ } else {
+ sCurrentAddress = currentAddress;
+ }
+ if (currentAddress != null && currentAddress.getCountryCode() != null) {
+ currentCity = checkNull(currentAddress.getLocality());
+ currentCountry = checkNull(currentAddress.getCountryCode());
+ currentAdminArea = checkNull(currentAddress.getAdminArea());
+ }
+ }
+
+ String closestCommonLocation = null;
+ String addr1Locality = checkNull(addr1.getLocality());
+ String addr2Locality = checkNull(addr2.getLocality());
+ String addr1AdminArea = checkNull(addr1.getAdminArea());
+ String addr2AdminArea = checkNull(addr2.getAdminArea());
+ String addr1CountryCode = checkNull(addr1.getCountryCode());
+ String addr2CountryCode = checkNull(addr2.getCountryCode());
+
+ if (currentCity.equals(addr1Locality) || currentCity.equals(addr2Locality)) {
+ String otherCity = currentCity;
+ if (currentCity.equals(addr1Locality)) {
+ otherCity = addr2Locality;
+ if (otherCity.length() == 0) {
+ otherCity = addr2AdminArea;
+ if (!currentCountry.equals(addr2CountryCode)) {
+ otherCity += " " + addr2CountryCode;
+ }
+ }
+ addr2Locality = addr1Locality;
+ addr2AdminArea = addr1AdminArea;
+ addr2CountryCode = addr1CountryCode;
+ } else {
+ otherCity = addr1Locality;
+ if (otherCity.length() == 0) {
+ otherCity = addr1AdminArea;
+ if (!currentCountry.equals(addr1CountryCode)) {
+ otherCity += " " + addr1CountryCode;
+ }
+ }
+ addr1Locality = addr2Locality;
+ addr1AdminArea = addr2AdminArea;
+ addr1CountryCode = addr2CountryCode;
+ }
+ closestCommonLocation = valueIfEqual(addr1.getAddressLine(0), addr2.getAddressLine(0));
+ if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
+ if (!currentCity.equals(otherCity)) {
+ closestCommonLocation += " - " + otherCity;
+ }
+ return closestCommonLocation;
+ }
+
+ // Compare thoroughfare (street address) next.
+ closestCommonLocation = valueIfEqual(addr1.getThoroughfare(), addr2.getThoroughfare());
+ if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
+ return closestCommonLocation;
+ }
+ }
+
+ // Compare the locality.
+ closestCommonLocation = valueIfEqual(addr1Locality, addr2Locality);
+ if (closestCommonLocation != null && !("".equals(closestCommonLocation))) {
+ String adminArea = addr1AdminArea;
+ String countryCode = addr1CountryCode;
+ if (adminArea != null && adminArea.length() > 0) {
+ if (!countryCode.equals(currentCountry)) {
+ closestCommonLocation += ", " + adminArea + " " + countryCode;
+ } else {
+ closestCommonLocation += ", " + adminArea;
+ }
+ }
+ return closestCommonLocation;
+ }
+
+ // If the admin area is the same as the current location, we hide it and
+ // instead show the city name.
+ if (currentAdminArea.equals(addr1AdminArea) && currentAdminArea.equals(addr2AdminArea)) {
+ if ("".equals(addr1Locality)) {
+ addr1Locality = addr2Locality;
+ }
+ if ("".equals(addr2Locality)) {
+ addr2Locality = addr1Locality;
+ }
+ if (!"".equals(addr1Locality)) {
+ if (addr1Locality.equals(addr2Locality)) {
+ closestCommonLocation = addr1Locality + ", " + currentAdminArea;
+ } else {
+ closestCommonLocation = addr1Locality + " - " + addr2Locality;
+ }
+ return closestCommonLocation;
+ }
+ }
+
+ // Just choose one of the localities if within a MAX_LOCALITY_MILE_RANGE
+ // mile radius.
+ float[] distanceFloat = new float[1];
+ Location.distanceBetween(setMinLatitude, setMinLongitude,
+ setMaxLatitude, setMaxLongitude, distanceFloat);
+ int distance = (int) GalleryUtils.toMile(distanceFloat[0]);
+ if (distance < MAX_LOCALITY_MILE_RANGE) {
+ // Try each of the points and just return the first one to have a
+ // valid address.
+ closestCommonLocation = getLocalityAdminForAddress(addr1, true);
+ if (closestCommonLocation != null) {
+ return closestCommonLocation;
+ }
+ closestCommonLocation = getLocalityAdminForAddress(addr2, true);
+ if (closestCommonLocation != null) {
+ return closestCommonLocation;
+ }
+ }
+
+ // Check the administrative area.
+ closestCommonLocation = valueIfEqual(addr1AdminArea, addr2AdminArea);
+ if (closestCommonLocation != null && !("".equals(closestCommonLocation))) {
+ String countryCode = addr1CountryCode;
+ if (!countryCode.equals(currentCountry)) {
+ if (countryCode != null && countryCode.length() > 0) {
+ closestCommonLocation += " " + countryCode;
+ }
+ }
+ return closestCommonLocation;
+ }
+
+ // Check the country codes.
+ closestCommonLocation = valueIfEqual(addr1CountryCode, addr2CountryCode);
+ if (closestCommonLocation != null && !("".equals(closestCommonLocation))) {
+ return closestCommonLocation;
+ }
+ // There is no intersection, let's choose a nicer name.
+ String addr1Country = addr1.getCountryName();
+ String addr2Country = addr2.getCountryName();
+ if (addr1Country == null)
+ addr1Country = addr1CountryCode;
+ if (addr2Country == null)
+ addr2Country = addr2CountryCode;
+ if (addr1Country == null || addr2Country == null)
+ return null;
+ if (addr1Country.length() > MAX_COUNTRY_NAME_LENGTH || addr2Country.length() > MAX_COUNTRY_NAME_LENGTH) {
+ closestCommonLocation = addr1CountryCode + " - " + addr2CountryCode;
+ } else {
+ closestCommonLocation = addr1Country + " - " + addr2Country;
+ }
+ return closestCommonLocation;
+ }
+
+ private String checkNull(String locality) {
+ if (locality == null)
+ return "";
+ if (locality.equals("null"))
+ return "";
+ return locality;
+ }
+
+ private String getLocalityAdminForAddress(final Address addr, final boolean approxLocation) {
+ if (addr == null)
+ return "";
+ String localityAdminStr = addr.getLocality();
+ if (localityAdminStr != null && !("null".equals(localityAdminStr))) {
+ if (approxLocation) {
+ // TODO: Uncomment these lines as soon as we may translations
+ // for Res.string.around.
+ // localityAdminStr =
+ // mContext.getResources().getString(Res.string.around) + " " +
+ // localityAdminStr;
+ }
+ String adminArea = addr.getAdminArea();
+ if (adminArea != null && adminArea.length() > 0) {
+ localityAdminStr += ", " + adminArea;
+ }
+ return localityAdminStr;
+ }
+ return null;
+ }
+
+ public Address lookupAddress(final double latitude, final double longitude,
+ boolean useCache) {
+ try {
+ long locationKey = (long) (((latitude + LAT_MAX) * 2 * LAT_MAX
+ + (longitude + LON_MAX)) * EARTH_RADIUS_METERS);
+ byte[] cachedLocation = null;
+ if (useCache && mGeoCache != null) {
+ cachedLocation = mGeoCache.lookup(locationKey);
+ }
+ Address address = null;
+ NetworkInfo networkInfo = mConnectivityManager.getActiveNetworkInfo();
+ if (cachedLocation == null || cachedLocation.length == 0) {
+ if (networkInfo == null || !networkInfo.isConnected()) {
+ return null;
+ }
+ List<Address> addresses = mGeocoder.getFromLocation(latitude, longitude, 1);
+ if (!addresses.isEmpty()) {
+ address = addresses.get(0);
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ DataOutputStream dos = new DataOutputStream(bos);
+ Locale locale = address.getLocale();
+ writeUTF(dos, locale.getLanguage());
+ writeUTF(dos, locale.getCountry());
+ writeUTF(dos, locale.getVariant());
+
+ writeUTF(dos, address.getThoroughfare());
+ int numAddressLines = address.getMaxAddressLineIndex();
+ dos.writeInt(numAddressLines);
+ for (int i = 0; i < numAddressLines; ++i) {
+ writeUTF(dos, address.getAddressLine(i));
+ }
+ writeUTF(dos, address.getFeatureName());
+ writeUTF(dos, address.getLocality());
+ writeUTF(dos, address.getAdminArea());
+ writeUTF(dos, address.getSubAdminArea());
+
+ writeUTF(dos, address.getCountryName());
+ writeUTF(dos, address.getCountryCode());
+ writeUTF(dos, address.getPostalCode());
+ writeUTF(dos, address.getPhone());
+ writeUTF(dos, address.getUrl());
+
+ dos.flush();
+ if (mGeoCache != null) {
+ mGeoCache.insert(locationKey, bos.toByteArray());
+ }
+ dos.close();
+ }
+ } else {
+ // Parsing the address from the byte stream.
+ DataInputStream dis = new DataInputStream(
+ new ByteArrayInputStream(cachedLocation));
+ String language = readUTF(dis);
+ String country = readUTF(dis);
+ String variant = readUTF(dis);
+ Locale locale = null;
+ if (language != null) {
+ if (country == null) {
+ locale = new Locale(language);
+ } else if (variant == null) {
+ locale = new Locale(language, country);
+ } else {
+ locale = new Locale(language, country, variant);
+ }
+ }
+ if (!locale.getLanguage().equals(Locale.getDefault().getLanguage())) {
+ dis.close();
+ return lookupAddress(latitude, longitude, false);
+ }
+ address = new Address(locale);
+
+ address.setThoroughfare(readUTF(dis));
+ int numAddressLines = dis.readInt();
+ for (int i = 0; i < numAddressLines; ++i) {
+ address.setAddressLine(i, readUTF(dis));
+ }
+ address.setFeatureName(readUTF(dis));
+ address.setLocality(readUTF(dis));
+ address.setAdminArea(readUTF(dis));
+ address.setSubAdminArea(readUTF(dis));
+
+ address.setCountryName(readUTF(dis));
+ address.setCountryCode(readUTF(dis));
+ address.setPostalCode(readUTF(dis));
+ address.setPhone(readUTF(dis));
+ address.setUrl(readUTF(dis));
+ dis.close();
+ }
+ return address;
+ } catch (Exception e) {
+ // Ignore.
+ }
+ return null;
+ }
+
+ private String valueIfEqual(String a, String b) {
+ return (a != null && b != null && a.equalsIgnoreCase(b)) ? a : null;
+ }
+
+ public static final void writeUTF(DataOutputStream dos, String string) throws IOException {
+ if (string == null) {
+ dos.writeUTF("");
+ } else {
+ dos.writeUTF(string);
+ }
+ }
+
+ public static final String readUTF(DataInputStream dis) throws IOException {
+ String retVal = dis.readUTF();
+ if (retVal.length() == 0)
+ return null;
+ return retVal;
+ }
+}
diff --git a/src/com/android/gallery3d/util/SaveVideoFileInfo.java b/src/com/android/gallery3d/util/SaveVideoFileInfo.java
new file mode 100644
index 000000000..c7e5e8568
--- /dev/null
+++ b/src/com/android/gallery3d/util/SaveVideoFileInfo.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+import java.io.File;
+
+public class SaveVideoFileInfo {
+ public File mFile = null;
+ public String mFileName = null;
+ // This the full directory path.
+ public File mDirectory = null;
+ // This is just the folder's name.
+ public String mFolderName = null;
+
+}
diff --git a/src/com/android/gallery3d/util/SaveVideoFileUtils.java b/src/com/android/gallery3d/util/SaveVideoFileUtils.java
new file mode 100644
index 000000000..10c41de90
--- /dev/null
+++ b/src/com/android/gallery3d/util/SaveVideoFileUtils.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore.Video;
+import android.provider.MediaStore.Video.VideoColumns;
+
+import com.android.gallery3d.filtershow.tools.SaveImage.ContentResolverQueryCallback;
+
+import java.io.File;
+import java.sql.Date;
+import java.text.SimpleDateFormat;
+
+public class SaveVideoFileUtils {
+ // This function can decide which folder to save the video file, and generate
+ // the needed information for the video file including filename.
+ public static SaveVideoFileInfo getDstMp4FileInfo(String fileNameFormat,
+ ContentResolver contentResolver, Uri uri, String defaultFolderName) {
+ SaveVideoFileInfo dstFileInfo = new SaveVideoFileInfo();
+ // Use the default save directory if the source directory cannot be
+ // saved.
+ dstFileInfo.mDirectory = getSaveDirectory(contentResolver, uri);
+ if ((dstFileInfo.mDirectory == null) || !dstFileInfo.mDirectory.canWrite()) {
+ dstFileInfo.mDirectory = new File(Environment.getExternalStorageDirectory(),
+ BucketNames.DOWNLOAD);
+ dstFileInfo.mFolderName = defaultFolderName;
+ } else {
+ dstFileInfo.mFolderName = dstFileInfo.mDirectory.getName();
+ }
+ dstFileInfo.mFileName = new SimpleDateFormat(fileNameFormat).format(
+ new Date(System.currentTimeMillis()));
+
+ dstFileInfo.mFile = new File(dstFileInfo.mDirectory, dstFileInfo.mFileName + ".mp4");
+ return dstFileInfo;
+ }
+
+ private static void querySource(ContentResolver contentResolver, Uri uri,
+ String[] projection, ContentResolverQueryCallback callback) {
+ Cursor cursor = null;
+ try {
+ cursor = contentResolver.query(uri, projection, null, null, null);
+ if ((cursor != null) && cursor.moveToNext()) {
+ callback.onCursorResult(cursor);
+ }
+ } catch (Exception e) {
+ // Ignore error for lacking the data column from the source.
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ private static File getSaveDirectory(ContentResolver contentResolver, Uri uri) {
+ final File[] dir = new File[1];
+ querySource(contentResolver, uri,
+ new String[] { VideoColumns.DATA },
+ new ContentResolverQueryCallback() {
+ @Override
+ public void onCursorResult(Cursor cursor) {
+ dir[0] = new File(cursor.getString(0)).getParentFile();
+ }
+ });
+ return dir[0];
+ }
+
+
+ /**
+ * Insert the content (saved file) with proper video properties.
+ */
+ public static Uri insertContent(SaveVideoFileInfo mDstFileInfo,
+ ContentResolver contentResolver, Uri uri ) {
+ long nowInMs = System.currentTimeMillis();
+ long nowInSec = nowInMs / 1000;
+ final ContentValues values = new ContentValues(13);
+ values.put(Video.Media.TITLE, mDstFileInfo.mFileName);
+ values.put(Video.Media.DISPLAY_NAME, mDstFileInfo.mFile.getName());
+ values.put(Video.Media.MIME_TYPE, "video/mp4");
+ values.put(Video.Media.DATE_TAKEN, nowInMs);
+ values.put(Video.Media.DATE_MODIFIED, nowInSec);
+ values.put(Video.Media.DATE_ADDED, nowInSec);
+ values.put(Video.Media.DATA, mDstFileInfo.mFile.getAbsolutePath());
+ values.put(Video.Media.SIZE, mDstFileInfo.mFile.length());
+ int durationMs = retriveVideoDurationMs(mDstFileInfo.mFile.getPath());
+ values.put(Video.Media.DURATION, durationMs);
+ // Copy the data taken and location info from src.
+ String[] projection = new String[] {
+ VideoColumns.DATE_TAKEN,
+ VideoColumns.LATITUDE,
+ VideoColumns.LONGITUDE,
+ VideoColumns.RESOLUTION,
+ };
+
+ // Copy some info from the source file.
+ querySource(contentResolver, uri, projection,
+ new ContentResolverQueryCallback() {
+ @Override
+ public void onCursorResult(Cursor cursor) {
+ long timeTaken = cursor.getLong(0);
+ if (timeTaken > 0) {
+ values.put(Video.Media.DATE_TAKEN, timeTaken);
+ }
+ double latitude = cursor.getDouble(1);
+ double longitude = cursor.getDouble(2);
+ // TODO: Change || to && after the default location
+ // issue is
+ // fixed.
+ if ((latitude != 0f) || (longitude != 0f)) {
+ values.put(Video.Media.LATITUDE, latitude);
+ values.put(Video.Media.LONGITUDE, longitude);
+ }
+ values.put(Video.Media.RESOLUTION, cursor.getString(3));
+
+ }
+ });
+
+ return contentResolver.insert(Video.Media.EXTERNAL_CONTENT_URI, values);
+ }
+
+ public static int retriveVideoDurationMs(String path) {
+ int durationMs = 0;
+ // Calculate the duration of the destination file.
+ MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+ retriever.setDataSource(path);
+ String duration = retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_DURATION);
+ if (duration != null) {
+ durationMs = Integer.parseInt(duration);
+ }
+ retriever.release();
+ return durationMs;
+ }
+
+}
diff --git a/src/com/android/gallery3d/util/UpdateHelper.java b/src/com/android/gallery3d/util/UpdateHelper.java
new file mode 100644
index 000000000..f76705d06
--- /dev/null
+++ b/src/com/android/gallery3d/util/UpdateHelper.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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.android.gallery3d.util;
+
+import com.android.gallery3d.common.Utils;
+
+public class UpdateHelper {
+
+ private boolean mUpdated = false;
+
+ public int update(int original, int update) {
+ if (original != update) {
+ mUpdated = true;
+ original = update;
+ }
+ return original;
+ }
+
+ public long update(long original, long update) {
+ if (original != update) {
+ mUpdated = true;
+ original = update;
+ }
+ return original;
+ }
+
+ public double update(double original, double update) {
+ if (original != update) {
+ mUpdated = true;
+ original = update;
+ }
+ return original;
+ }
+
+ public <T> T update(T original, T update) {
+ if (!Utils.equals(original, update)) {
+ mUpdated = true;
+ original = update;
+ }
+ return original;
+ }
+
+ public boolean isUpdated() {
+ return mUpdated;
+ }
+}