/* * 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.fastergallery.util; import android.annotation.TargetApi; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.ContentResolver; import android.content.ContentValues; 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.database.Cursor; 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.provider.MediaStore.Images.ImageColumns; import android.util.DisplayMetrics; import android.util.Log; import android.view.WindowManager; import com.android.fastergallery.app.Gallery; import com.android.fastergallery.app.PackagesMonitor; import com.android.fastergallery.common.ApiHelper; import com.android.fastergallery.data.DataManager; import com.android.fastergallery.data.LocalImage; import com.android.fastergallery.data.MediaItem; import com.android.fastergallery.ui.TiledScreenNail; import com.android.fastergallery.util.ThreadPool.CancelListener; import com.android.fastergallery.util.ThreadPool.JobContext; import com.android.fastergallery.R; import java.io.File; import java.util.ArrayList; 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; private static final int INDEX_CAPTION = 0; private static final int INDEX_MIME_TYPE = 1; private static final int INDEX_LATITUDE = 2; private static final int INDEX_LONGITUDE = 3; private static final int INDEX_DATE_TAKEN = 4; private static final int INDEX_DATE_ADDED = 5; private static final int INDEX_DATE_MODIFIED = 6; private static final int INDEX_DATA = 7; private static final int INDEX_ORIENTATION = 8; private static final int INDEX_SIZE = 9; static final String[] PROJECTION = { ImageColumns.TITLE, // 0 ImageColumns.MIME_TYPE, // 1 ImageColumns.LATITUDE, // 2 ImageColumns.LONGITUDE, // 3 ImageColumns.DATE_TAKEN, // 4 ImageColumns.DATE_ADDED, // 5 ImageColumns.DATE_MODIFIED, // 6 ImageColumns.DATA, // 7 ImageColumns.ORIENTATION, // 8 ImageColumns.SIZE, // 9 }; 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); } public static void hideImageDir(String path) { File root = Environment.getExternalStorageDirectory(); if (root == null || path == null) { return; } File full = new File(root.getAbsolutePath()+path+File.separator+".nomedia"); try { full.createNewFile(); }catch (Exception e) { e.printStackTrace(); } } public static boolean RenameImageDir(File fullPath, String newName) { if (fullPath.getName().equalsIgnoreCase(newName)) { return false; } File newDir = new File(fullPath.getParent() + File.separator + newName); return fullPath.renameTo(newDir); } public static void insertToMediaProvide(ArrayList<ContentValues> aList, String oldname, String newName, ContentResolver cr) { String path = null; for (ContentValues value : aList) { path = value.getAsString(ImageColumns.DATA); path = path.replace(File.separator+oldname, File.separator+newName); value.remove(ImageColumns.DATA); value.put(ImageColumns.DATA, path); value.put(ImageColumns.BUCKET_ID, new File(path).hashCode()); value.put(ImageColumns.BUCKET_DISPLAY_NAME, newName); cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, value); path = null; } } public static ArrayList<ContentValues> getCurrentValues(Uri uri, int bucketId, ContentResolver cr, String whereClause, String orderClause) { ArrayList<ContentValues> ac = new ArrayList<ContentValues>(); Cursor dataCursor = cr.query(uri, PROJECTION, whereClause, new String[] {String.valueOf(bucketId)}, orderClause); if (dataCursor != null) { while(dataCursor.moveToNext()) { ContentValues value = new ContentValues(); value.put(ImageColumns.TITLE, dataCursor.getString(INDEX_CAPTION)); value.put(ImageColumns.MIME_TYPE, dataCursor.getString(INDEX_MIME_TYPE)); value.put(ImageColumns.LATITUDE, dataCursor.getDouble(INDEX_LATITUDE)); value.put(ImageColumns.LONGITUDE, dataCursor.getDouble(INDEX_LONGITUDE)); value.put(ImageColumns.DATE_TAKEN, dataCursor.getLong(INDEX_DATE_TAKEN)); value.put(ImageColumns.DATE_ADDED, dataCursor.getLong(INDEX_DATE_ADDED)); value.put(ImageColumns.DATE_MODIFIED, dataCursor.getLong(INDEX_DATE_MODIFIED)); value.put(ImageColumns.DATA, dataCursor.getString(INDEX_DATA)); value.put(ImageColumns.ORIENTATION, dataCursor.getInt(INDEX_ORIENTATION)); value.put(ImageColumns.SIZE, dataCursor.getLong(INDEX_SIZE)); ac.add(value); } dataCursor.close(); } return ac; } }