/* * 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.GalleryActivity; 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.IntentHelper; 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(); Intent cameraIntent = IntentHelper.getCameraIntent(context); List<ResolveInfo> apps = pm.queryIntentActivities(cameraIntent, 0); sCameraAvailableInitialized = true; sCameraAvailable = !apps.isEmpty(); 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); try { context.startActivity(intent); } catch (ActivityNotFoundException e) { // This will only occur if Camera was disabled while Gallery is open // since we cache our availability check. Just abort the attempt. Log.e(TAG, "Camera activity previously detected but cannot be found", e); } } public static void startGalleryActivity(Context context) { Intent intent = new Intent(context, GalleryActivity.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); } }