1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.gallery3d.util;
18
19import com.android.gallery3d.R;
20import com.android.gallery3d.app.PackagesMonitor;
21import com.android.gallery3d.data.DataManager;
22import com.android.gallery3d.data.MediaItem;
23import com.android.gallery3d.util.ThreadPool.CancelListener;
24import com.android.gallery3d.util.ThreadPool.JobContext;
25
26import android.app.Activity;
27import android.content.ActivityNotFoundException;
28import android.content.ComponentName;
29import android.content.Context;
30import android.content.Intent;
31import android.content.SharedPreferences;
32import android.content.pm.PackageManager;
33import android.content.pm.ResolveInfo;
34import android.net.Uri;
35import android.os.ConditionVariable;
36import android.os.Environment;
37import android.os.StatFs;
38import android.preference.PreferenceManager;
39import android.provider.MediaStore;
40import android.util.DisplayMetrics;
41import android.util.Log;
42import android.view.WindowManager;
43
44import java.util.Arrays;
45import java.util.List;
46import java.util.Locale;
47
48public class GalleryUtils {
49    private static final String TAG = "GalleryUtils";
50    private static final String MAPS_PACKAGE_NAME = "com.google.android.apps.maps";
51    private static final String MAPS_CLASS_NAME = "com.google.android.maps.MapsActivity";
52
53    private static final String MIME_TYPE_IMAGE = "image/*";
54    private static final String MIME_TYPE_VIDEO = "video/*";
55    private static final String MIME_TYPE_ALL = "*/*";
56    private static final String DIR_TYPE_IMAGE = "vnd.android.cursor.dir/image";
57    private static final String DIR_TYPE_VIDEO = "vnd.android.cursor.dir/video";
58
59    private static final String PREFIX_PHOTO_EDITOR_UPDATE = "editor-update-";
60    private static final String PREFIX_HAS_PHOTO_EDITOR = "has-editor-";
61
62    private static final String KEY_CAMERA_UPDATE = "camera-update";
63    private static final String KEY_HAS_CAMERA = "has-camera";
64
65    private static Context sContext;
66
67
68    static float sPixelDensity = -1f;
69
70    public static void initialize(Context context) {
71        sContext = context;
72        if (sPixelDensity < 0) {
73            DisplayMetrics metrics = new DisplayMetrics();
74            WindowManager wm = (WindowManager)
75                    context.getSystemService(Context.WINDOW_SERVICE);
76            wm.getDefaultDisplay().getMetrics(metrics);
77            sPixelDensity = metrics.density;
78        }
79    }
80
81    public static float dpToPixel(float dp) {
82        return sPixelDensity * dp;
83    }
84
85    public static int dpToPixel(int dp) {
86        return Math.round(dpToPixel((float) dp));
87    }
88
89    public static int meterToPixel(float meter) {
90        // 1 meter = 39.37 inches, 1 inch = 160 dp.
91        return Math.round(dpToPixel(meter * 39.37f * 160));
92    }
93
94    public static byte[] getBytes(String in) {
95        byte[] result = new byte[in.length() * 2];
96        int output = 0;
97        for (char ch : in.toCharArray()) {
98            result[output++] = (byte) (ch & 0xFF);
99            result[output++] = (byte) (ch >> 8);
100        }
101        return result;
102    }
103
104    // Below are used the detect using database in the render thread. It only
105    // works most of the time, but that's ok because it's for debugging only.
106
107    private static volatile Thread sCurrentThread;
108    private static volatile boolean sWarned;
109
110    public static void setRenderThread() {
111        sCurrentThread = Thread.currentThread();
112    }
113
114    public static void assertNotInRenderThread() {
115        if (!sWarned) {
116            if (Thread.currentThread() == sCurrentThread) {
117                sWarned = true;
118                Log.w(TAG, new Throwable("Should not do this in render thread"));
119            }
120        }
121    }
122
123    private static final double RAD_PER_DEG = Math.PI / 180.0;
124    private static final double EARTH_RADIUS_METERS = 6367000.0;
125
126    public static double fastDistanceMeters(double latRad1, double lngRad1,
127            double latRad2, double lngRad2) {
128       if ((Math.abs(latRad1 - latRad2) > RAD_PER_DEG)
129             || (Math.abs(lngRad1 - lngRad2) > RAD_PER_DEG)) {
130           return accurateDistanceMeters(latRad1, lngRad1, latRad2, lngRad2);
131       }
132       // Approximate sin(x) = x.
133       double sineLat = (latRad1 - latRad2);
134
135       // Approximate sin(x) = x.
136       double sineLng = (lngRad1 - lngRad2);
137
138       // Approximate cos(lat1) * cos(lat2) using
139       // cos((lat1 + lat2)/2) ^ 2
140       double cosTerms = Math.cos((latRad1 + latRad2) / 2.0);
141       cosTerms = cosTerms * cosTerms;
142       double trigTerm = sineLat * sineLat + cosTerms * sineLng * sineLng;
143       trigTerm = Math.sqrt(trigTerm);
144
145       // Approximate arcsin(x) = x
146       return EARTH_RADIUS_METERS * trigTerm;
147    }
148
149    public static double accurateDistanceMeters(double lat1, double lng1,
150            double lat2, double lng2) {
151        double dlat = Math.sin(0.5 * (lat2 - lat1));
152        double dlng = Math.sin(0.5 * (lng2 - lng1));
153        double x = dlat * dlat + dlng * dlng * Math.cos(lat1) * Math.cos(lat2);
154        return (2 * Math.atan2(Math.sqrt(x), Math.sqrt(Math.max(0.0,
155                1.0 - x)))) * EARTH_RADIUS_METERS;
156    }
157
158
159    public static final double toMile(double meter) {
160        return meter / 1609;
161    }
162
163    // For debugging, it will block the caller for timeout millis.
164    public static void fakeBusy(JobContext jc, int timeout) {
165        final ConditionVariable cv = new ConditionVariable();
166        jc.setCancelListener(new CancelListener() {
167            public void onCancel() {
168                cv.open();
169            }
170        });
171        cv.block(timeout);
172        jc.setCancelListener(null);
173    }
174
175    public static boolean isEditorAvailable(Context context, String mimeType) {
176        int version = PackagesMonitor.getPackagesVersion(context);
177
178        String updateKey = PREFIX_PHOTO_EDITOR_UPDATE + mimeType;
179        String hasKey = PREFIX_HAS_PHOTO_EDITOR + mimeType;
180
181        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
182        if (prefs.getInt(updateKey, 0) != version) {
183            PackageManager packageManager = context.getPackageManager();
184            List<ResolveInfo> infos = packageManager.queryIntentActivities(
185                    new Intent(Intent.ACTION_EDIT).setType(mimeType), 0);
186            prefs.edit().putInt(updateKey, version)
187                        .putBoolean(hasKey, !infos.isEmpty())
188                        .commit();
189        }
190
191        return prefs.getBoolean(hasKey, true);
192    }
193
194    public static boolean isCameraAvailable(Context context) {
195        int version = PackagesMonitor.getPackagesVersion(context);
196        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
197        if (prefs.getInt(KEY_CAMERA_UPDATE, 0) != version) {
198            PackageManager packageManager = context.getPackageManager();
199            List<ResolveInfo> infos = packageManager.queryIntentActivities(
200                    new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA), 0);
201            prefs.edit().putInt(KEY_CAMERA_UPDATE, version)
202                        .putBoolean(KEY_HAS_CAMERA, !infos.isEmpty())
203                        .commit();
204        }
205        return prefs.getBoolean(KEY_HAS_CAMERA, true);
206    }
207
208    public static boolean isValidLocation(double latitude, double longitude) {
209        // TODO: change || to && after we fix the default location issue
210        return (latitude != MediaItem.INVALID_LATLNG || longitude != MediaItem.INVALID_LATLNG);
211    }
212
213    public static String formatLatitudeLongitude(String format, double latitude,
214            double longitude) {
215        // We need to specify the locale otherwise it may go wrong in some language
216        // (e.g. Locale.FRENCH)
217        return String.format(Locale.ENGLISH, format, latitude, longitude);
218    }
219
220    public static void showOnMap(Context context, double latitude, double longitude) {
221        try {
222            // We don't use "geo:latitude,longitude" because it only centers
223            // the MapView to the specified location, but we need a marker
224            // for further operations (routing to/from).
225            // The q=(lat, lng) syntax is suggested by geo-team.
226            String uri = formatLatitudeLongitude("http://maps.google.com/maps?f=q&q=(%f,%f)",
227                    latitude, longitude);
228            ComponentName compName = new ComponentName(MAPS_PACKAGE_NAME,
229                    MAPS_CLASS_NAME);
230            Intent mapsIntent = new Intent(Intent.ACTION_VIEW,
231                    Uri.parse(uri)).setComponent(compName);
232            context.startActivity(mapsIntent);
233        } catch (ActivityNotFoundException e) {
234            // Use the "geo intent" if no GMM is installed
235            Log.e(TAG, "GMM activity not found!", e);
236            String url = formatLatitudeLongitude("geo:%f,%f", latitude, longitude);
237            Intent mapsIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
238            context.startActivity(mapsIntent);
239        }
240    }
241
242    public static void setViewPointMatrix(
243            float matrix[], float x, float y, float z) {
244        // The matrix is
245        // -z,  0,  x,  0
246        //  0, -z,  y,  0
247        //  0,  0,  1,  0
248        //  0,  0,  1, -z
249        Arrays.fill(matrix, 0, 16, 0);
250        matrix[0] = matrix[5] = matrix[15] = -z;
251        matrix[8] = x;
252        matrix[9] = y;
253        matrix[10] = matrix[11] = 1;
254    }
255
256    public static int getBucketId(String path) {
257        return path.toLowerCase().hashCode();
258    }
259
260    // Returns a (localized) string for the given duration (in seconds).
261    public static String formatDuration(final Context context, int duration) {
262        int h = duration / 3600;
263        int m = (duration - h * 3600) / 60;
264        int s = duration - (h * 3600 + m * 60);
265        String durationValue;
266        if (h == 0) {
267            durationValue = String.format(context.getString(R.string.details_ms), m, s);
268        } else {
269            durationValue = String.format(context.getString(R.string.details_hms), h, m, s);
270        }
271        return durationValue;
272    }
273
274    public static void setSpinnerVisibility(final Activity activity,
275            final boolean visible) {
276        SpinnerVisibilitySetter.getInstance(activity).setSpinnerVisibility(visible);
277    }
278
279    public static int determineTypeBits(Context context, Intent intent) {
280        int typeBits = 0;
281        String type = intent.resolveType(context);
282
283        if (MIME_TYPE_ALL.equals(type)) {
284            typeBits = DataManager.INCLUDE_ALL;
285        } else if (MIME_TYPE_IMAGE.equals(type) ||
286                DIR_TYPE_IMAGE.equals(type)) {
287            typeBits = DataManager.INCLUDE_IMAGE;
288        } else if (MIME_TYPE_VIDEO.equals(type) ||
289                DIR_TYPE_VIDEO.equals(type)) {
290            typeBits = DataManager.INCLUDE_VIDEO;
291        } else {
292            typeBits = DataManager.INCLUDE_ALL;
293        }
294
295        if (intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false)) {
296            typeBits |= DataManager.INCLUDE_LOCAL_ONLY;
297        }
298
299        return typeBits;
300    }
301
302    public static int getSelectionModePrompt(int typeBits) {
303        if ((typeBits & DataManager.INCLUDE_VIDEO) != 0) {
304            return (typeBits & DataManager.INCLUDE_IMAGE) == 0
305                    ? R.string.select_video
306                    : R.string.select_item;
307        }
308        return R.string.select_image;
309    }
310
311    public static boolean hasSpaceForSize(long size) {
312        String state = Environment.getExternalStorageState();
313        if (!Environment.MEDIA_MOUNTED.equals(state)) {
314            return false;
315        }
316
317        String path = Environment.getExternalStorageDirectory().getPath();
318        try {
319            StatFs stat = new StatFs(path);
320            return stat.getAvailableBlocks() * (long) stat.getBlockSize() > size;
321        } catch (Exception e) {
322            Log.i(TAG, "Fail to access external storage", e);
323        }
324        return false;
325    }
326
327    public static void assertInMainThread() {
328        if (Thread.currentThread() == sContext.getMainLooper().getThread()) {
329            throw new AssertionError();
330        }
331    }
332
333    public static void doubleToRational(double value, long[] output) {
334        // error is a magic number to control the tollerance of error
335        doubleToRational(value, output, 0.00001);
336    }
337
338    private static void doubleToRational(double value, long[] output, double error) {
339        long number = (long) value;
340        value -= number;
341        if (value < 0.000001 || error > 1) {
342            output[0] = (int) (number + value + 0.5);
343            output[1] = 1;
344        } else {
345            doubleToRational(1.0 / value, output, error / value);
346            number = number * output[0] + output[1];
347            output[1] = output[0];
348            output[0] = number;
349        }
350    }
351
352    public static boolean isPanorama(MediaItem item) {
353        if (item == null) return false;
354        int w = item.getWidth();
355        int h = item.getHeight();
356        return (h > 0 && w / h >= 2);
357    }
358}
359