diff options
429 files changed, 50655 insertions, 0 deletions
diff --git a/Android.mk b/Android.mk new file mode 100644 index 000000000..0f5170f64 --- /dev/null +++ b/Android.mk @@ -0,0 +1,24 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := optional + +LOCAL_STATIC_JAVA_LIBRARIES := android-support-v13 +LOCAL_STATIC_JAVA_LIBRARIES += com.android.gallery3d.common2 + +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_SRC_FILES += $(call all-java-files-under, src_pd) + +LOCAL_PACKAGE_NAME := Gallery2 + +LOCAL_OVERRIDES_PACKAGES := Gallery Gallery3D GalleryNew3D + +# We mark this out until Mtp and MediaMetadataRetriever is unhidden. +LOCAL_SDK_VERSION := current + +LOCAL_PROGUARD_FLAG_FILES := proguard.flags + +include $(BUILD_PACKAGE) + +# Use the following include to make our test apk. +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 000000000..f568265d1 --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,239 @@ +<?xml version="1.0" encoding="utf-8"?> + +<manifest android:versionCode="30682" + android:versionName="1.1.30682" + xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.gallery3d"> + + <original-package android:name="com.android.gallery3d" /> + + <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> + <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <uses-permission android:name="android.permission.GET_ACCOUNTS" /> + <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" /> + <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> + <uses-permission android:name="android.permission.SET_WALLPAPER" /> + <uses-permission android:name="android.permission.USE_CREDENTIALS" /> + <uses-permission android:name="android.permission.VIBRATE" /> + <uses-permission android:name="android.permission.WAKE_LOCK" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" /> + <uses-permission android:name="android.permission.WRITE_SETTINGS" /> + + <supports-screens android:smallScreens="false" + android:normalScreens="true" android:largeScreens="true" + android:anyDensity="true" /> + + <application android:icon="@mipmap/ic_launcher_gallery" android:label="@string/app_name" + android:name="com.android.gallery3d.app.GalleryAppImpl" + android:theme="@style/Theme.Gallery"> + <activity android:name="com.android.gallery3d.app.MovieActivity" + android:label="@string/movie_view_label" + android:configChanges="orientation|keyboardHidden|screenSize"> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="rtsp" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" /> + <data android:scheme="https" /> + <data android:scheme="content" /> + <data android:scheme="file" /> + <data android:mimeType="video/mpeg4" /> + <data android:mimeType="video/mp4" /> + <data android:mimeType="video/3gp" /> + <data android:mimeType="video/3gpp" /> + <data android:mimeType="video/3gpp2" /> + <data android:mimeType="video/webm" /> + <data android:mimeType="application/sdp" /> + </intent-filter> + <intent-filter> + !-- HTTP live support --> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" /> + <data android:mimeType="audio/x-mpegurl" /> + <data android:mimeType="audio/mpegurl" /> + <data android:mimeType="application/vnd.apple.mpegurl" /> + <data android:mimeType="application/x-mpegurl" /> + </intent-filter> + </activity> + <activity android:name="com.android.gallery3d.app.Gallery" android:label="@string/app_name" + android:configChanges="keyboardHidden|orientation|screenSize"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.GET_CONTENT" /> + <category android:name="android.intent.category.OPENABLE" /> + <data android:mimeType="vnd.android.cursor.dir/image" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.GET_CONTENT" /> + <category android:name="android.intent.category.OPENABLE" /> + <category android:name="android.intent.category.DEFAULT" /> + <data android:mimeType="image/*" /> + <data android:mimeType="video/*" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <data android:mimeType="vnd.android.cursor.dir/image" /> + <data android:mimeType="vnd.android.cursor.dir/video" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <action android:name="com.android.camera.action.REVIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="" /> + <data android:scheme="http" /> + <data android:scheme="https" /> + <data android:scheme="content" /> + <data android:scheme="file" /> + <data android:mimeType="image/bmp" /> + <data android:mimeType="image/jpeg" /> + <data android:mimeType="image/gif" /> + <data android:mimeType="image/png" /> + <data android:mimeType="image/x-ms-bmp" /> + <data android:mimeType="image/vnd.wap.wbmp" /> + </intent-filter> + <intent-filter> + <action android:name="com.android.camera.action.REVIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" /> + <data android:scheme="https" /> + <data android:scheme="content" /> + <data android:scheme="file" /> + <data android:mimeType="video/mpeg4" /> + <data android:mimeType="video/mp4" /> + <data android:mimeType="video/3gp" /> + <data android:mimeType="video/3gpp" /> + <data android:mimeType="video/3gpp2" /> + <data android:mimeType="application/sdp" /> + </intent-filter> + <!-- We do NOT support the PICK intent, we add these intent-filter for + backward compatibility. Handle it as GET_CONTENT. --> + <intent-filter> + <action android:name="android.intent.action.PICK" /> + <category android:name="android.intent.category.DEFAULT" /> + <data android:mimeType="image/*" /> + <data android:mimeType="video/*" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.PICK" /> + <category android:name="android.intent.category.DEFAULT" /> + <data android:mimeType="vnd.android.cursor.dir/image" /> + <data android:mimeType="vnd.android.cursor.dir/video" /> + </intent-filter> + </activity> + + <!-- This activity receives USB_DEVICE_ATTACHED Intents and springboards to main Gallery activity. --> + <activity android:name="com.android.gallery3d.app.UsbDeviceActivity" android:label="@string/app_name" + android:taskAffinity="" + android:launchMode="singleInstance"> + <intent-filter> + <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" /> + </intent-filter> + <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" + android:resource="@xml/device_filter" /> + </activity> + + <activity android:name="com.android.gallery3d.app.Wallpaper" + android:configChanges="keyboardHidden|orientation|screenSize" + android:theme="@style/android:Theme.Translucent.NoTitleBar"> + <intent-filter android:label="@string/camera_setas_wallpaper"> + <action android:name="android.intent.action.ATTACH_DATA" /> + <data android:mimeType="image/*" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + <intent-filter android:label="@string/app_name"> + <action android:name="android.intent.action.SET_WALLPAPER" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + <meta-data android:name="android.wallpaper.preview" + android:resource="@xml/wallpaper_picker_preview" /> + </activity> + <activity android:name="com.android.gallery3d.app.CropImage" + android:configChanges="keyboardHidden|orientation|screenSize" + android:label="@string/crop_label"> + <intent-filter android:label="@string/crop_label"> + <action android:name="com.android.camera.action.CROP" /> + <data android:scheme="http" /> + <data android:scheme="https" /> + <data android:scheme="content" /> + <data android:scheme="file" /> + <data android:scheme="" /> + <data android:mimeType="image/*" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.ALTERNATIVE" /> + <category android:name="android.intent.category.SELECTED_ALTERNATIVE" /> + </intent-filter> + </activity> + + <activity android:name="com.android.gallery3d.app.SlideshowDream" + android:label="@string/slideshow_dream_name" + android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" + android:hardwareAccelerated="true" + > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.DREAM" /> + </intent-filter> + </activity> + + <activity android:name="com.android.gallery3d.settings.GallerySettings" + android:configChanges="orientation|keyboardHidden|screenSize" /> + + <provider android:name="com.android.gallery3d.provider.GalleryProvider" + android:syncable="false" + android:grantUriPermissions="true" + android:authorities="com.android.gallery3d.provider" /> + <activity android:name="com.android.gallery3d.widget.WidgetClickHandler" /> + <activity android:name="com.android.gallery3d.app.DialogPicker" + android:configChanges="keyboardHidden|orientation|screenSize" + android:theme="@style/DialogPickerTheme"/> + <activity android:name="com.android.gallery3d.app.AlbumPicker" + android:configChanges="keyboardHidden|orientation|screenSize" + android:theme="@style/DialogPickerTheme"/> + <activity android:name="com.android.gallery3d.widget.WidgetTypeChooser" + android:configChanges="keyboardHidden|orientation|screenSize" + android:theme="@style/DialogPickerTheme"/> + + <receiver android:name="com.android.gallery3d.widget.WidgetProvider" + android:label="@string/appwidget_title"> + <intent-filter> + <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> + </intent-filter> + <meta-data android:name="android.appwidget.provider" + android:resource="@xml/widget_info" /> + </receiver> + <receiver android:name="com.android.gallery3d.app.PackagesMonitor"> + <intent-filter> + <action android:name="android.intent.action.PACKAGE_ADDED"/> + <action android:name="android.intent.action.PACKAGE_REMOVED"/> + <data android:scheme="package"/> + </intent-filter> + </receiver> + <service android:name="com.android.gallery3d.widget.WidgetService" + android:permission="android.permission.BIND_REMOTEVIEWS"/> + <activity android:name="com.android.gallery3d.widget.WidgetConfigure" + android:configChanges="keyboardHidden|orientation|screenSize" + android:theme="@style/android:Theme.Translucent.NoTitleBar"> + <intent-filter> + <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/gallerycommon/Android.mk b/gallerycommon/Android.mk new file mode 100644 index 000000000..a942de289 --- /dev/null +++ b/gallerycommon/Android.mk @@ -0,0 +1,27 @@ +# Copyright 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. + +LOCAL_PATH := $(call my-dir) + +# Build the com.android.emailcommon static library. At the moment, this includes +# the emailcommon files themselves plus everything under src/org (apache code). All of our +# AIDL files are also compiled into the static library + +include $(CLEAR_VARS) + +LOCAL_MODULE := com.android.gallery3d.common2 +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_SDK_VERSION := 8 + +include $(BUILD_STATIC_JAVA_LIBRARY) diff --git a/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java b/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java new file mode 100644 index 000000000..04cdc6142 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java @@ -0,0 +1,285 @@ +/* + * 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.common; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.os.Build; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class BitmapUtils { + private static final String TAG = "BitmapUtils"; + public static final int UNCONSTRAINED = -1; + private static final int COMPRESS_JPEG_QUALITY = 90; + + private BitmapUtils(){} + + /* + * Compute the sample size as a function of minSideLength + * and maxNumOfPixels. + * minSideLength is used to specify that minimal width or height of a + * bitmap. + * maxNumOfPixels is used to specify the maximal size in pixels that is + * tolerable in terms of memory usage. + * + * The function returns a sample size based on the constraints. + * Both size and minSideLength can be passed in as UNCONSTRAINED, + * which indicates no care of the corresponding constraint. + * The functions prefers returning a sample size that + * generates a smaller bitmap, unless minSideLength = UNCONSTRAINED. + * + * Also, the function rounds up the sample size to a power of 2 or multiple + * of 8 because BitmapFactory only honors sample size this way. + * For example, BitmapFactory downsamples an image by 2 even though the + * request is 3. So we round up the sample size to avoid OOM. + */ + public static int computeSampleSize(int width, int height, + int minSideLength, int maxNumOfPixels) { + int initialSize = computeInitialSampleSize( + width, height, minSideLength, maxNumOfPixels); + + return initialSize <= 8 + ? Utils.nextPowerOf2(initialSize) + : (initialSize + 7) / 8 * 8; + } + + private static int computeInitialSampleSize(int w, int h, + int minSideLength, int maxNumOfPixels) { + if (maxNumOfPixels == UNCONSTRAINED + && minSideLength == UNCONSTRAINED) return 1; + + int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 : + (int) Math.ceil(Math.sqrt((double) (w * h) / maxNumOfPixels)); + + if (minSideLength == UNCONSTRAINED) { + return lowerBound; + } else { + int sampleSize = Math.min(w / minSideLength, h / minSideLength); + return Math.max(sampleSize, lowerBound); + } + } + + // This computes a sample size which makes the longer side at least + // minSideLength long. If that's not possible, return 1. + public static int computeSampleSizeLarger(int w, int h, + int minSideLength) { + int initialSize = Math.min(w / minSideLength, h / minSideLength); + if (initialSize <= 1) return 1; + + return initialSize <= 8 + ? Utils.prevPowerOf2(initialSize) + : initialSize / 8 * 8; + } + + // Fin the min x that 1 / x <= scale + public static int computeSampleSizeLarger(float scale) { + int initialSize = (int) Math.floor(1f / scale); + if (initialSize <= 1) return 1; + + return initialSize <= 8 + ? Utils.prevPowerOf2(initialSize) + : initialSize / 8 * 8; + } + + // Find the max x that 1 / x >= scale. + public static int computeSampleSize(float scale) { + Utils.assertTrue(scale > 0); + int initialSize = Math.max(1, (int) Math.ceil(1 / scale)); + return initialSize <= 8 + ? Utils.nextPowerOf2(initialSize) + : (initialSize + 7) / 8 * 8; + } + + public static Bitmap resizeDownToPixels( + Bitmap bitmap, int targetPixels, boolean recycle) { + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + float scale = (float) Math.sqrt( + (double) targetPixels / (width * height)); + if (scale >= 1.0f) return bitmap; + return resizeBitmapByScale(bitmap, scale, recycle); + } + + public static Bitmap resizeBitmapByScale( + Bitmap bitmap, float scale, boolean recycle) { + int width = Math.round(bitmap.getWidth() * scale); + int height = Math.round(bitmap.getHeight() * scale); + if (width == bitmap.getWidth() + && height == bitmap.getHeight()) return bitmap; + Bitmap target = Bitmap.createBitmap(width, height, getConfig(bitmap)); + Canvas canvas = new Canvas(target); + canvas.scale(scale, scale); + Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG); + canvas.drawBitmap(bitmap, 0, 0, paint); + if (recycle) bitmap.recycle(); + return target; + } + + private static Bitmap.Config getConfig(Bitmap bitmap) { + Bitmap.Config config = bitmap.getConfig(); + if (config == null) { + config = Bitmap.Config.ARGB_8888; + } + return config; + } + + public static Bitmap resizeDownBySideLength( + Bitmap bitmap, int maxLength, boolean recycle) { + int srcWidth = bitmap.getWidth(); + int srcHeight = bitmap.getHeight(); + float scale = Math.min( + (float) maxLength / srcWidth, (float) maxLength / srcHeight); + if (scale >= 1.0f) return bitmap; + return resizeBitmapByScale(bitmap, scale, recycle); + } + + // Crops a square from the center of the original image. + public static Bitmap cropCenter(Bitmap bitmap, boolean recycle) { + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + if (width == height) return bitmap; + int size = Math.min(width, height); + + Bitmap target = Bitmap.createBitmap(size, size, getConfig(bitmap)); + Canvas canvas = new Canvas(target); + canvas.translate((size - width) / 2, (size - height) / 2); + Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG); + canvas.drawBitmap(bitmap, 0, 0, paint); + if (recycle) bitmap.recycle(); + return target; + } + + public static Bitmap resizeDownAndCropCenter(Bitmap bitmap, int size, + boolean recycle) { + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + int minSide = Math.min(w, h); + if (w == h && minSide <= size) return bitmap; + size = Math.min(size, minSide); + + float scale = Math.max((float) size / bitmap.getWidth(), + (float) size / bitmap.getHeight()); + Bitmap target = Bitmap.createBitmap(size, size, getConfig(bitmap)); + int width = Math.round(scale * bitmap.getWidth()); + int height = Math.round(scale * bitmap.getHeight()); + Canvas canvas = new Canvas(target); + canvas.translate((size - width) / 2f, (size - height) / 2f); + canvas.scale(scale, scale); + Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG); + canvas.drawBitmap(bitmap, 0, 0, paint); + if (recycle) bitmap.recycle(); + return target; + } + + public static void recycleSilently(Bitmap bitmap) { + if (bitmap == null) return; + try { + bitmap.recycle(); + } catch (Throwable t) { + Log.w(TAG, "unable recycle bitmap", t); + } + } + + public static Bitmap rotateBitmap(Bitmap source, int rotation, boolean recycle) { + int w = source.getWidth(); + int h = source.getHeight(); + Matrix m = new Matrix(); + m.postRotate(rotation); + Bitmap bitmap = Bitmap.createBitmap(source, 0, 0, w, h, m, true); + if (recycle) source.recycle(); + return bitmap; + } + + public static Bitmap createVideoThumbnail(String filePath) { + // MediaMetadataRetriever is available on API Level 8 + // but is hidden until API Level 10 + Class<?> clazz = null; + Object instance = null; + try { + clazz = Class.forName("android.media.MediaMetadataRetriever"); + instance = clazz.newInstance(); + + Method method = clazz.getMethod("setDataSource", String.class); + method.invoke(instance, filePath); + + // The method name changes between API Level 9 and 10. + if (Build.VERSION.SDK_INT <= 9) { + return (Bitmap) clazz.getMethod("captureFrame").invoke(instance); + } else { + return (Bitmap) clazz.getMethod("getFrameAtTime").invoke(instance); + } + } catch (IllegalArgumentException ex) { + // Assume this is a corrupt video file + } catch (RuntimeException ex) { + // Assume this is a corrupt video file. + } catch (InstantiationException e) { + Log.e(TAG, "createVideoThumbnail", e); + } catch (InvocationTargetException e) { + Log.e(TAG, "createVideoThumbnail", e); + } catch (ClassNotFoundException e) { + Log.e(TAG, "createVideoThumbnail", e); + } catch (NoSuchMethodException e) { + Log.e(TAG, "createVideoThumbnail", e); + } catch (IllegalAccessException e) { + Log.e(TAG, "createVideoThumbnail", e); + } finally { + try { + if (instance != null) { + clazz.getMethod("release").invoke(instance); + } + } catch (Exception ignored) { + } + } + return null; + } + + public static byte[] compressBitmap(Bitmap bitmap) { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.JPEG, + COMPRESS_JPEG_QUALITY, os); + return os.toByteArray(); + } + + public static boolean isSupportedByRegionDecoder(String mimeType) { + if (mimeType == null) return false; + mimeType = mimeType.toLowerCase(); + return mimeType.startsWith("image/") && + (!mimeType.equals("image/gif") && !mimeType.endsWith("bmp")); + } + + public static boolean isRotationSupported(String mimeType) { + if (mimeType == null) return false; + mimeType = mimeType.toLowerCase(); + return mimeType.equals("image/jpeg"); + } + + public static byte[] compressToBytes(Bitmap bitmap, int quality) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(65536); + bitmap.compress(CompressFormat.JPEG, quality, baos); + return baos.toByteArray(); + + } + + +} diff --git a/gallerycommon/src/com/android/gallery3d/common/BlobCache.java b/gallerycommon/src/com/android/gallery3d/common/BlobCache.java new file mode 100644 index 000000000..19a2e3090 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/BlobCache.java @@ -0,0 +1,653 @@ +/* + * 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. + */ + +// This is an on-disk cache which maps a 64-bits key to a byte array. +// +// It consists of three files: one index file and two data files. One of the +// data files is "active", and the other is "inactive". New entries are +// appended into the active region until it reaches the size limit. At that +// point the active file and the inactive file are swapped, and the new active +// file is truncated to empty (and the index for that file is also cleared). +// The index is a hash table with linear probing. When the load factor reaches +// 0.5, it does the same thing like when the size limit is reached. +// +// The index file format: (all numbers are stored in little-endian) +// [0] Magic number: 0xB3273030 +// [4] MaxEntries: Max number of hash entries per region. +// [8] MaxBytes: Max number of data bytes per region (including header). +// [12] ActiveRegion: The active growing region: 0 or 1. +// [16] ActiveEntries: The number of hash entries used in the active region. +// [20] ActiveBytes: The number of data bytes used in the active region. +// [24] Version number. +// [28] Checksum of [0..28). +// [32] Hash entries for region 0. The size is X = (12 * MaxEntries bytes). +// [32 + X] Hash entries for region 1. The size is also X. +// +// Each hash entry is 12 bytes: 8 bytes key and 4 bytes offset into the data +// file. The offset is 0 when the slot is free. Note that 0 is a valid value +// for key. The keys are used directly as index into a hash table, so they +// should be suitably distributed. +// +// Each data file stores data for one region. The data file is concatenated +// blobs followed by the magic number 0xBD248510. +// +// The blob format: +// [0] Key of this blob +// [8] Checksum of this blob +// [12] Offset of this blob +// [16] Length of this blob (not including header) +// [20] Blob +// +// Below are the interface for BlobCache. The instance of this class does not +// support concurrent use by multiple threads. +// +// public BlobCache(String path, int maxEntries, int maxBytes, boolean reset) throws IOException; +// public void insert(long key, byte[] data) throws IOException; +// public byte[] lookup(long key) throws IOException; +// public void lookup(LookupRequest req) throws IOException; +// public void close(); +// public void syncIndex(); +// public void syncAll(); +// public static void deleteFiles(String path); +// +package com.android.gallery3d.common; + +import android.util.Log; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteOrder; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.util.zip.Adler32; + +public class BlobCache { + private static final String TAG = "BlobCache"; + + private static final int MAGIC_INDEX_FILE = 0xB3273030; + private static final int MAGIC_DATA_FILE = 0xBD248510; + + // index header offset + private static final int IH_MAGIC = 0; + private static final int IH_MAX_ENTRIES = 4; + private static final int IH_MAX_BYTES = 8; + private static final int IH_ACTIVE_REGION = 12; + private static final int IH_ACTIVE_ENTRIES = 16; + private static final int IH_ACTIVE_BYTES = 20; + private static final int IH_VERSION = 24; + private static final int IH_CHECKSUM = 28; + private static final int INDEX_HEADER_SIZE = 32; + + private static final int DATA_HEADER_SIZE = 4; + + // blob header offset + private static final int BH_KEY = 0; + private static final int BH_CHECKSUM = 8; + private static final int BH_OFFSET = 12; + private static final int BH_LENGTH = 16; + private static final int BLOB_HEADER_SIZE = 20; + + private RandomAccessFile mIndexFile; + private RandomAccessFile mDataFile0; + private RandomAccessFile mDataFile1; + private FileChannel mIndexChannel; + private MappedByteBuffer mIndexBuffer; + + private int mMaxEntries; + private int mMaxBytes; + private int mActiveRegion; + private int mActiveEntries; + private int mActiveBytes; + private int mVersion; + + private RandomAccessFile mActiveDataFile; + private RandomAccessFile mInactiveDataFile; + private int mActiveHashStart; + private int mInactiveHashStart; + private byte[] mIndexHeader = new byte[INDEX_HEADER_SIZE]; + private byte[] mBlobHeader = new byte[BLOB_HEADER_SIZE]; + private Adler32 mAdler32 = new Adler32(); + + // Creates the cache. Three files will be created: + // path + ".idx", path + ".0", and path + ".1" + // The ".0" file and the ".1" file each stores data for a region. Each of + // them can grow to the size specified by maxBytes. The maxEntries parameter + // specifies the maximum number of entries each region can have. If the + // "reset" parameter is true, the cache will be cleared before use. + public BlobCache(String path, int maxEntries, int maxBytes, boolean reset) + throws IOException { + this(path, maxEntries, maxBytes, reset, 0); + } + + public BlobCache(String path, int maxEntries, int maxBytes, boolean reset, + int version) throws IOException { + mIndexFile = new RandomAccessFile(path + ".idx", "rw"); + mDataFile0 = new RandomAccessFile(path + ".0", "rw"); + mDataFile1 = new RandomAccessFile(path + ".1", "rw"); + mVersion = version; + + if (!reset && loadIndex()) { + return; + } + + resetCache(maxEntries, maxBytes); + + if (!loadIndex()) { + closeAll(); + throw new IOException("unable to load index"); + } + } + + // Delete the files associated with the given path previously created + // by the BlobCache constructor. + public static void deleteFiles(String path) { + deleteFileSilently(path + ".idx"); + deleteFileSilently(path + ".0"); + deleteFileSilently(path + ".1"); + } + + private static void deleteFileSilently(String path) { + try { + new File(path).delete(); + } catch (Throwable t) { + // ignore; + } + } + + // Close the cache. All resources are released. No other method should be + // called after this is called. + public void close() { + syncAll(); + closeAll(); + } + + private void closeAll() { + closeSilently(mIndexChannel); + closeSilently(mIndexFile); + closeSilently(mDataFile0); + closeSilently(mDataFile1); + } + + // Returns true if loading index is successful. After this method is called, + // mIndexHeader and index header in file should be kept sync. + private boolean loadIndex() { + try { + mIndexFile.seek(0); + mDataFile0.seek(0); + mDataFile1.seek(0); + + byte[] buf = mIndexHeader; + if (mIndexFile.read(buf) != INDEX_HEADER_SIZE) { + Log.w(TAG, "cannot read header"); + return false; + } + + if (readInt(buf, IH_MAGIC) != MAGIC_INDEX_FILE) { + Log.w(TAG, "cannot read header magic"); + return false; + } + + if (readInt(buf, IH_VERSION) != mVersion) { + Log.w(TAG, "version mismatch"); + return false; + } + + mMaxEntries = readInt(buf, IH_MAX_ENTRIES); + mMaxBytes = readInt(buf, IH_MAX_BYTES); + mActiveRegion = readInt(buf, IH_ACTIVE_REGION); + mActiveEntries = readInt(buf, IH_ACTIVE_ENTRIES); + mActiveBytes = readInt(buf, IH_ACTIVE_BYTES); + + int sum = readInt(buf, IH_CHECKSUM); + if (checkSum(buf, 0, IH_CHECKSUM) != sum) { + Log.w(TAG, "header checksum does not match"); + return false; + } + + // Sanity check + if (mMaxEntries <= 0) { + Log.w(TAG, "invalid max entries"); + return false; + } + if (mMaxBytes <= 0) { + Log.w(TAG, "invalid max bytes"); + return false; + } + if (mActiveRegion != 0 && mActiveRegion != 1) { + Log.w(TAG, "invalid active region"); + return false; + } + if (mActiveEntries < 0 || mActiveEntries > mMaxEntries) { + Log.w(TAG, "invalid active entries"); + return false; + } + if (mActiveBytes < DATA_HEADER_SIZE || mActiveBytes > mMaxBytes) { + Log.w(TAG, "invalid active bytes"); + return false; + } + if (mIndexFile.length() != + INDEX_HEADER_SIZE + mMaxEntries * 12 * 2) { + Log.w(TAG, "invalid index file length"); + return false; + } + + // Make sure data file has magic + byte[] magic = new byte[4]; + if (mDataFile0.read(magic) != 4) { + Log.w(TAG, "cannot read data file magic"); + return false; + } + if (readInt(magic, 0) != MAGIC_DATA_FILE) { + Log.w(TAG, "invalid data file magic"); + return false; + } + if (mDataFile1.read(magic) != 4) { + Log.w(TAG, "cannot read data file magic"); + return false; + } + if (readInt(magic, 0) != MAGIC_DATA_FILE) { + Log.w(TAG, "invalid data file magic"); + return false; + } + + // Map index file to memory + mIndexChannel = mIndexFile.getChannel(); + mIndexBuffer = mIndexChannel.map(FileChannel.MapMode.READ_WRITE, + 0, mIndexFile.length()); + mIndexBuffer.order(ByteOrder.LITTLE_ENDIAN); + + setActiveVariables(); + return true; + } catch (IOException ex) { + Log.e(TAG, "loadIndex failed.", ex); + return false; + } + } + + private void setActiveVariables() throws IOException { + mActiveDataFile = (mActiveRegion == 0) ? mDataFile0 : mDataFile1; + mInactiveDataFile = (mActiveRegion == 1) ? mDataFile0 : mDataFile1; + mActiveDataFile.setLength(mActiveBytes); + mActiveDataFile.seek(mActiveBytes); + + mActiveHashStart = INDEX_HEADER_SIZE; + mInactiveHashStart = INDEX_HEADER_SIZE; + + if (mActiveRegion == 0) { + mInactiveHashStart += mMaxEntries * 12; + } else { + mActiveHashStart += mMaxEntries * 12; + } + } + + private void resetCache(int maxEntries, int maxBytes) throws IOException { + mIndexFile.setLength(0); // truncate to zero the index + mIndexFile.setLength(INDEX_HEADER_SIZE + maxEntries * 12 * 2); + mIndexFile.seek(0); + byte[] buf = mIndexHeader; + writeInt(buf, IH_MAGIC, MAGIC_INDEX_FILE); + writeInt(buf, IH_MAX_ENTRIES, maxEntries); + writeInt(buf, IH_MAX_BYTES, maxBytes); + writeInt(buf, IH_ACTIVE_REGION, 0); + writeInt(buf, IH_ACTIVE_ENTRIES, 0); + writeInt(buf, IH_ACTIVE_BYTES, DATA_HEADER_SIZE); + writeInt(buf, IH_VERSION, mVersion); + writeInt(buf, IH_CHECKSUM, checkSum(buf, 0, IH_CHECKSUM)); + mIndexFile.write(buf); + // This is only needed if setLength does not zero the extended part. + // writeZero(mIndexFile, maxEntries * 12 * 2); + + mDataFile0.setLength(0); + mDataFile1.setLength(0); + mDataFile0.seek(0); + mDataFile1.seek(0); + writeInt(buf, 0, MAGIC_DATA_FILE); + mDataFile0.write(buf, 0, 4); + mDataFile1.write(buf, 0, 4); + } + + // Flip the active region and the inactive region. + private void flipRegion() throws IOException { + mActiveRegion = 1 - mActiveRegion; + mActiveEntries = 0; + mActiveBytes = DATA_HEADER_SIZE; + + writeInt(mIndexHeader, IH_ACTIVE_REGION, mActiveRegion); + writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries); + writeInt(mIndexHeader, IH_ACTIVE_BYTES, mActiveBytes); + updateIndexHeader(); + + setActiveVariables(); + clearHash(mActiveHashStart); + syncIndex(); + } + + // Sync mIndexHeader to the index file. + private void updateIndexHeader() { + writeInt(mIndexHeader, IH_CHECKSUM, + checkSum(mIndexHeader, 0, IH_CHECKSUM)); + mIndexBuffer.position(0); + mIndexBuffer.put(mIndexHeader); + } + + // Clear the hash table starting from the specified offset. + private void clearHash(int hashStart) { + byte[] zero = new byte[1024]; + mIndexBuffer.position(hashStart); + for (int count = mMaxEntries * 12; count > 0;) { + int todo = Math.min(count, 1024); + mIndexBuffer.put(zero, 0, todo); + count -= todo; + } + } + + // Inserts a (key, data) pair into the cache. + public void insert(long key, byte[] data) throws IOException { + if (DATA_HEADER_SIZE + BLOB_HEADER_SIZE + data.length > mMaxBytes) { + throw new RuntimeException("blob is too large!"); + } + + if (mActiveBytes + BLOB_HEADER_SIZE + data.length > mMaxBytes + || mActiveEntries * 2 >= mMaxEntries) { + flipRegion(); + } + + if (!lookupInternal(key, mActiveHashStart)) { + // If we don't have an existing entry with the same key, increase + // the entry count. + mActiveEntries++; + writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries); + } + + insertInternal(key, data, data.length); + updateIndexHeader(); + } + + // Appends the data to the active file. It also updates the hash entry. + // The proper hash entry (suitable for insertion or replacement) must be + // pointed by mSlotOffset. + private void insertInternal(long key, byte[] data, int length) + throws IOException { + byte[] header = mBlobHeader; + int sum = checkSum(data); + writeLong(header, BH_KEY, key); + writeInt(header, BH_CHECKSUM, sum); + writeInt(header, BH_OFFSET, mActiveBytes); + writeInt(header, BH_LENGTH, length); + mActiveDataFile.write(header); + mActiveDataFile.write(data, 0, length); + + mIndexBuffer.putLong(mSlotOffset, key); + mIndexBuffer.putInt(mSlotOffset + 8, mActiveBytes); + mActiveBytes += BLOB_HEADER_SIZE + length; + writeInt(mIndexHeader, IH_ACTIVE_BYTES, mActiveBytes); + } + + public static class LookupRequest { + public long key; // input: the key to find + public byte[] buffer; // input/output: the buffer to store the blob + public int length; // output: the length of the blob + } + + // This method is for one-off lookup. For repeated lookup, use the version + // accepting LookupRequest to avoid repeated memory allocation. + private LookupRequest mLookupRequest = new LookupRequest(); + public byte[] lookup(long key) throws IOException { + mLookupRequest.key = key; + mLookupRequest.buffer = null; + if (lookup(mLookupRequest)) { + return mLookupRequest.buffer; + } else { + return null; + } + } + + // Returns true if the associated blob for the given key is available. + // The blob is stored in the buffer pointed by req.buffer, and the length + // is in stored in the req.length variable. + // + // The user can input a non-null value in req.buffer, and this method will + // try to use that buffer. If that buffer is not large enough, this method + // will allocate a new buffer and assign it to req.buffer. + // + // This method tries not to throw IOException even if the data file is + // corrupted, but it can still throw IOException if things get strange. + public boolean lookup(LookupRequest req) throws IOException { + // Look up in the active region first. + if (lookupInternal(req.key, mActiveHashStart)) { + if (getBlob(mActiveDataFile, mFileOffset, req)) { + return true; + } + } + + // We want to copy the data from the inactive file to the active file + // if it's available. So we keep the offset of the hash entry so we can + // avoid looking it up again. + int insertOffset = mSlotOffset; + + // Look up in the inactive region. + if (lookupInternal(req.key, mInactiveHashStart)) { + if (getBlob(mInactiveDataFile, mFileOffset, req)) { + // If we don't have enough space to insert this blob into + // the active file, just return it. + if (mActiveBytes + BLOB_HEADER_SIZE + req.length > mMaxBytes + || mActiveEntries * 2 >= mMaxEntries) { + return true; + } + // Otherwise copy it over. + mSlotOffset = insertOffset; + try { + insertInternal(req.key, req.buffer, req.length); + mActiveEntries++; + writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries); + updateIndexHeader(); + } catch (Throwable t) { + Log.e(TAG, "cannot copy over"); + } + return true; + } + } + + return false; + } + + + // Copies the blob for the specified offset in the specified file to + // req.buffer. If req.buffer is null or too small, allocate a buffer and + // assign it to req.buffer. + // Returns false if the blob is not available (either the index file is + // not sync with the data file, or one of them is corrupted). The length + // of the blob is stored in the req.length variable. + private boolean getBlob(RandomAccessFile file, int offset, + LookupRequest req) throws IOException { + byte[] header = mBlobHeader; + long oldPosition = file.getFilePointer(); + try { + file.seek(offset); + if (file.read(header) != BLOB_HEADER_SIZE) { + Log.w(TAG, "cannot read blob header"); + return false; + } + long blobKey = readLong(header, BH_KEY); + if (blobKey != req.key) { + Log.w(TAG, "blob key does not match: " + blobKey); + return false; + } + int sum = readInt(header, BH_CHECKSUM); + int blobOffset = readInt(header, BH_OFFSET); + if (blobOffset != offset) { + Log.w(TAG, "blob offset does not match: " + blobOffset); + return false; + } + int length = readInt(header, BH_LENGTH); + if (length < 0 || length > mMaxBytes - offset - BLOB_HEADER_SIZE) { + Log.w(TAG, "invalid blob length: " + length); + return false; + } + if (req.buffer == null || req.buffer.length < length) { + req.buffer = new byte[length]; + } + + byte[] blob = req.buffer; + req.length = length; + + if (file.read(blob, 0, length) != length) { + Log.w(TAG, "cannot read blob data"); + return false; + } + if (checkSum(blob, 0, length) != sum) { + Log.w(TAG, "blob checksum does not match: " + sum); + return false; + } + return true; + } catch (Throwable t) { + Log.e(TAG, "getBlob failed.", t); + return false; + } finally { + file.seek(oldPosition); + } + } + + // Tries to look up a key in the specified hash region. + // Returns true if the lookup is successful. + // The slot offset in the index file is saved in mSlotOffset. If the lookup + // is successful, it's the slot found. Otherwise it's the slot suitable for + // insertion. + // If the lookup is successful, the file offset is also saved in + // mFileOffset. + private int mSlotOffset; + private int mFileOffset; + private boolean lookupInternal(long key, int hashStart) { + int slot = (int) (key % mMaxEntries); + if (slot < 0) slot += mMaxEntries; + int slotBegin = slot; + while (true) { + int offset = hashStart + slot * 12; + long candidateKey = mIndexBuffer.getLong(offset); + int candidateOffset = mIndexBuffer.getInt(offset + 8); + if (candidateOffset == 0) { + mSlotOffset = offset; + return false; + } else if (candidateKey == key) { + mSlotOffset = offset; + mFileOffset = candidateOffset; + return true; + } else { + if (++slot >= mMaxEntries) { + slot = 0; + } + if (slot == slotBegin) { + Log.w(TAG, "corrupted index: clear the slot."); + mIndexBuffer.putInt(hashStart + slot * 12 + 8, 0); + } + } + } + } + + public void syncIndex() { + try { + mIndexBuffer.force(); + } catch (Throwable t) { + Log.w(TAG, "sync index failed", t); + } + } + + public void syncAll() { + syncIndex(); + try { + mDataFile0.getFD().sync(); + } catch (Throwable t) { + Log.w(TAG, "sync data file 0 failed", t); + } + try { + mDataFile1.getFD().sync(); + } catch (Throwable t) { + Log.w(TAG, "sync data file 1 failed", t); + } + } + + // This is for testing only. + // + // Returns the active count (mActiveEntries). This also verifies that + // the active count matches matches what's inside the hash region. + int getActiveCount() { + int count = 0; + for (int i = 0; i < mMaxEntries; i++) { + int offset = mActiveHashStart + i * 12; + long candidateKey = mIndexBuffer.getLong(offset); + int candidateOffset = mIndexBuffer.getInt(offset + 8); + if (candidateOffset != 0) ++count; + } + if (count == mActiveEntries) { + return count; + } else { + Log.e(TAG, "wrong active count: " + mActiveEntries + " vs " + count); + return -1; // signal failure. + } + } + + int checkSum(byte[] data) { + mAdler32.reset(); + mAdler32.update(data); + return (int) mAdler32.getValue(); + } + + int checkSum(byte[] data, int offset, int nbytes) { + mAdler32.reset(); + mAdler32.update(data, offset, nbytes); + return (int) mAdler32.getValue(); + } + + static void closeSilently(Closeable c) { + if (c == null) return; + try { + c.close(); + } catch (Throwable t) { + // do nothing + } + } + + static int readInt(byte[] buf, int offset) { + return (buf[offset] & 0xff) + | ((buf[offset + 1] & 0xff) << 8) + | ((buf[offset + 2] & 0xff) << 16) + | ((buf[offset + 3] & 0xff) << 24); + } + + static long readLong(byte[] buf, int offset) { + long result = buf[offset + 7] & 0xff; + for (int i = 6; i >= 0; i--) { + result = (result << 8) | (buf[offset + i] & 0xff); + } + return result; + } + + static void writeInt(byte[] buf, int offset, int value) { + for (int i = 0; i < 4; i++) { + buf[offset + i] = (byte) (value & 0xff); + value >>= 8; + } + } + + static void writeLong(byte[] buf, int offset, long value) { + for (int i = 0; i < 8; i++) { + buf[offset + i] = (byte) (value & 0xff); + value >>= 8; + } + } +} diff --git a/gallerycommon/src/com/android/gallery3d/common/Entry.java b/gallerycommon/src/com/android/gallery3d/common/Entry.java new file mode 100644 index 000000000..b8cc51205 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/Entry.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2009 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.common; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +public abstract class Entry { + public static final String[] ID_PROJECTION = { "_id" }; + + public static interface Columns { + public static final String ID = "_id"; + } + + // The primary key of the entry. + @Column("_id") + public long id = 0; + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface Table { + String value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface Column { + String value(); + + boolean indexed() default false; + + boolean fullText() default false; + + String defaultValue() default ""; + } + + public void clear() { + id = 0; + } +} diff --git a/gallerycommon/src/com/android/gallery3d/common/EntrySchema.java b/gallerycommon/src/com/android/gallery3d/common/EntrySchema.java new file mode 100644 index 000000000..d652ac98a --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/EntrySchema.java @@ -0,0 +1,529 @@ +/* + * Copyright (C) 2009 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.common; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.text.TextUtils; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.util.ArrayList; + +public final class EntrySchema { + @SuppressWarnings("unused") + private static final String TAG = "EntrySchema"; + + private static final int TYPE_STRING = 0; + private static final int TYPE_BOOLEAN = 1; + private static final int TYPE_SHORT = 2; + private static final int TYPE_INT = 3; + private static final int TYPE_LONG = 4; + private static final int TYPE_FLOAT = 5; + private static final int TYPE_DOUBLE = 6; + private static final int TYPE_BLOB = 7; + private static final String SQLITE_TYPES[] = { + "TEXT", "INTEGER", "INTEGER", "INTEGER", "INTEGER", "REAL", "REAL", "NONE" }; + + private static final String FULL_TEXT_INDEX_SUFFIX = "_fulltext"; + + private final String mTableName; + private final ColumnInfo[] mColumnInfo; + private final String[] mProjection; + private final boolean mHasFullTextIndex; + + public EntrySchema(Class<? extends Entry> clazz) { + // Get table and column metadata from reflection. + ColumnInfo[] columns = parseColumnInfo(clazz); + mTableName = parseTableName(clazz); + mColumnInfo = columns; + + // Cache the list of projection columns and check for full-text columns. + String[] projection = {}; + boolean hasFullTextIndex = false; + if (columns != null) { + projection = new String[columns.length]; + for (int i = 0; i != columns.length; ++i) { + ColumnInfo column = columns[i]; + projection[i] = column.name; + if (column.fullText) { + hasFullTextIndex = true; + } + } + } + mProjection = projection; + mHasFullTextIndex = hasFullTextIndex; + } + + public String getTableName() { + return mTableName; + } + + public ColumnInfo[] getColumnInfo() { + return mColumnInfo; + } + + public String[] getProjection() { + return mProjection; + } + + public int getColumnIndex(String columnName) { + for (ColumnInfo column : mColumnInfo) { + if (column.name.equals(columnName)) { + return column.projectionIndex; + } + } + return -1; + } + + private ColumnInfo getColumn(String columnName) { + int index = getColumnIndex(columnName); + return (index < 0) ? null : mColumnInfo[index]; + } + + private void logExecSql(SQLiteDatabase db, String sql) { + db.execSQL(sql); + } + + public <T extends Entry> T cursorToObject(Cursor cursor, T object) { + try { + for (ColumnInfo column : mColumnInfo) { + int columnIndex = column.projectionIndex; + Field field = column.field; + switch (column.type) { + case TYPE_STRING: + field.set(object, cursor.isNull(columnIndex) + ? null + : cursor.getString(columnIndex)); + break; + case TYPE_BOOLEAN: + field.setBoolean(object, cursor.getShort(columnIndex) == 1); + break; + case TYPE_SHORT: + field.setShort(object, cursor.getShort(columnIndex)); + break; + case TYPE_INT: + field.setInt(object, cursor.getInt(columnIndex)); + break; + case TYPE_LONG: + field.setLong(object, cursor.getLong(columnIndex)); + break; + case TYPE_FLOAT: + field.setFloat(object, cursor.getFloat(columnIndex)); + break; + case TYPE_DOUBLE: + field.setDouble(object, cursor.getDouble(columnIndex)); + break; + case TYPE_BLOB: + field.set(object, cursor.isNull(columnIndex) + ? null + : cursor.getBlob(columnIndex)); + break; + } + } + return object; + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private void setIfNotNull(Field field, Object object, Object value) + throws IllegalAccessException { + if (value != null) field.set(object, value); + } + + /** + * Converts the ContentValues to the object. The ContentValues may not + * contain values for all the fields in the object. + */ + public <T extends Entry> T valuesToObject(ContentValues values, T object) { + try { + for (ColumnInfo column : mColumnInfo) { + String columnName = column.name; + Field field = column.field; + switch (column.type) { + case TYPE_STRING: + setIfNotNull(field, object, values.getAsString(columnName)); + break; + case TYPE_BOOLEAN: + setIfNotNull(field, object, values.getAsBoolean(columnName)); + break; + case TYPE_SHORT: + setIfNotNull(field, object, values.getAsShort(columnName)); + break; + case TYPE_INT: + setIfNotNull(field, object, values.getAsInteger(columnName)); + break; + case TYPE_LONG: + setIfNotNull(field, object, values.getAsLong(columnName)); + break; + case TYPE_FLOAT: + setIfNotNull(field, object, values.getAsFloat(columnName)); + break; + case TYPE_DOUBLE: + setIfNotNull(field, object, values.getAsDouble(columnName)); + break; + case TYPE_BLOB: + setIfNotNull(field, object, values.getAsByteArray(columnName)); + break; + } + } + return object; + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + public void objectToValues(Entry object, ContentValues values) { + try { + for (ColumnInfo column : mColumnInfo) { + String columnName = column.name; + Field field = column.field; + switch (column.type) { + case TYPE_STRING: + values.put(columnName, (String) field.get(object)); + break; + case TYPE_BOOLEAN: + values.put(columnName, field.getBoolean(object)); + break; + case TYPE_SHORT: + values.put(columnName, field.getShort(object)); + break; + case TYPE_INT: + values.put(columnName, field.getInt(object)); + break; + case TYPE_LONG: + values.put(columnName, field.getLong(object)); + break; + case TYPE_FLOAT: + values.put(columnName, field.getFloat(object)); + break; + case TYPE_DOUBLE: + values.put(columnName, field.getDouble(object)); + break; + case TYPE_BLOB: + values.put(columnName, (byte[]) field.get(object)); + break; + } + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + public String toDebugString(Entry entry) { + try { + StringBuilder sb = new StringBuilder(); + sb.append("ID=").append(entry.id); + for (ColumnInfo column : mColumnInfo) { + String columnName = column.name; + Field field = column.field; + Object value = field.get(entry); + sb.append(" ").append(columnName).append("=") + .append((value == null) ? "null" : value.toString()); + } + return sb.toString(); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + public String toDebugString(Entry entry, String... columnNames) { + try { + StringBuilder sb = new StringBuilder(); + sb.append("ID=").append(entry.id); + for (String columnName : columnNames) { + ColumnInfo column = getColumn(columnName); + Field field = column.field; + Object value = field.get(entry); + sb.append(" ").append(columnName).append("=") + .append((value == null) ? "null" : value.toString()); + } + return sb.toString(); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + public Cursor queryAll(SQLiteDatabase db) { + return db.query(mTableName, mProjection, null, null, null, null, null); + } + + public boolean queryWithId(SQLiteDatabase db, long id, Entry entry) { + Cursor cursor = db.query(mTableName, mProjection, "_id=?", + new String[] {Long.toString(id)}, null, null, null); + boolean success = false; + if (cursor.moveToFirst()) { + cursorToObject(cursor, entry); + success = true; + } + cursor.close(); + return success; + } + + public long insertOrReplace(SQLiteDatabase db, Entry entry) { + ContentValues values = new ContentValues(); + objectToValues(entry, values); + if (entry.id == 0) { + values.remove("_id"); + } + long id = db.replace(mTableName, "_id", values); + entry.id = id; + return id; + } + + public boolean deleteWithId(SQLiteDatabase db, long id) { + return db.delete(mTableName, "_id=?", new String[] { Long.toString(id) }) == 1; + } + + public void createTables(SQLiteDatabase db) { + // Wrapped class must have a @Table.Definition. + String tableName = mTableName; + Utils.assertTrue(tableName != null); + + // Add the CREATE TABLE statement for the main table. + StringBuilder sql = new StringBuilder("CREATE TABLE "); + sql.append(tableName); + sql.append(" (_id INTEGER PRIMARY KEY AUTOINCREMENT"); + for (ColumnInfo column : mColumnInfo) { + if (!column.isId()) { + sql.append(','); + sql.append(column.name); + sql.append(' '); + sql.append(SQLITE_TYPES[column.type]); + if (!TextUtils.isEmpty(column.defaultValue)) { + sql.append(" DEFAULT "); + sql.append(column.defaultValue); + } + } + } + sql.append(");"); + logExecSql(db, sql.toString()); + sql.setLength(0); + + // Create indexes for all indexed columns. + for (ColumnInfo column : mColumnInfo) { + // Create an index on the indexed columns. + if (column.indexed) { + sql.append("CREATE INDEX "); + sql.append(tableName); + sql.append("_index_"); + sql.append(column.name); + sql.append(" ON "); + sql.append(tableName); + sql.append(" ("); + sql.append(column.name); + sql.append(");"); + logExecSql(db, sql.toString()); + sql.setLength(0); + } + } + + if (mHasFullTextIndex) { + // Add an FTS virtual table if using full-text search. + String ftsTableName = tableName + FULL_TEXT_INDEX_SUFFIX; + sql.append("CREATE VIRTUAL TABLE "); + sql.append(ftsTableName); + sql.append(" USING FTS3 (_id INTEGER PRIMARY KEY"); + for (ColumnInfo column : mColumnInfo) { + if (column.fullText) { + // Add the column to the FTS table. + String columnName = column.name; + sql.append(','); + sql.append(columnName); + sql.append(" TEXT"); + } + } + sql.append(");"); + logExecSql(db, sql.toString()); + sql.setLength(0); + + // Build an insert statement that will automatically keep the FTS + // table in sync. + StringBuilder insertSql = new StringBuilder("INSERT OR REPLACE INTO "); + insertSql.append(ftsTableName); + insertSql.append(" (_id"); + for (ColumnInfo column : mColumnInfo) { + if (column.fullText) { + insertSql.append(','); + insertSql.append(column.name); + } + } + insertSql.append(") VALUES (new._id"); + for (ColumnInfo column : mColumnInfo) { + if (column.fullText) { + insertSql.append(",new."); + insertSql.append(column.name); + } + } + insertSql.append(");"); + String insertSqlString = insertSql.toString(); + + // Add an insert trigger. + sql.append("CREATE TRIGGER "); + sql.append(tableName); + sql.append("_insert_trigger AFTER INSERT ON "); + sql.append(tableName); + sql.append(" FOR EACH ROW BEGIN "); + sql.append(insertSqlString); + sql.append("END;"); + logExecSql(db, sql.toString()); + sql.setLength(0); + + // Add an update trigger. + sql.append("CREATE TRIGGER "); + sql.append(tableName); + sql.append("_update_trigger AFTER UPDATE ON "); + sql.append(tableName); + sql.append(" FOR EACH ROW BEGIN "); + sql.append(insertSqlString); + sql.append("END;"); + logExecSql(db, sql.toString()); + sql.setLength(0); + + // Add a delete trigger. + sql.append("CREATE TRIGGER "); + sql.append(tableName); + sql.append("_delete_trigger AFTER DELETE ON "); + sql.append(tableName); + sql.append(" FOR EACH ROW BEGIN DELETE FROM "); + sql.append(ftsTableName); + sql.append(" WHERE _id = old._id; END;"); + logExecSql(db, sql.toString()); + sql.setLength(0); + } + } + + public void dropTables(SQLiteDatabase db) { + String tableName = mTableName; + StringBuilder sql = new StringBuilder("DROP TABLE IF EXISTS "); + sql.append(tableName); + sql.append(';'); + logExecSql(db, sql.toString()); + sql.setLength(0); + + if (mHasFullTextIndex) { + sql.append("DROP TABLE IF EXISTS "); + sql.append(tableName); + sql.append(FULL_TEXT_INDEX_SUFFIX); + sql.append(';'); + logExecSql(db, sql.toString()); + } + + } + + public void deleteAll(SQLiteDatabase db) { + StringBuilder sql = new StringBuilder("DELETE FROM "); + sql.append(mTableName); + sql.append(";"); + logExecSql(db, sql.toString()); + } + + private String parseTableName(Class<? extends Object> clazz) { + // Check for a table annotation. + Entry.Table table = clazz.getAnnotation(Entry.Table.class); + if (table == null) { + return null; + } + + // Return the table name. + return table.value(); + } + + private ColumnInfo[] parseColumnInfo(Class<? extends Object> clazz) { + ArrayList<ColumnInfo> columns = new ArrayList<ColumnInfo>(); + while (clazz != null) { + parseColumnInfo(clazz, columns); + clazz = clazz.getSuperclass(); + } + + // Return a list. + ColumnInfo[] columnList = new ColumnInfo[columns.size()]; + columns.toArray(columnList); + return columnList; + } + + private void parseColumnInfo(Class<? extends Object> clazz, ArrayList<ColumnInfo> columns) { + // Gather metadata from each annotated field. + Field[] fields = clazz.getDeclaredFields(); // including non-public fields + for (int i = 0; i != fields.length; ++i) { + // Get column metadata from the annotation. + Field field = fields[i]; + Entry.Column info = ((AnnotatedElement) field).getAnnotation(Entry.Column.class); + if (info == null) continue; + + // Determine the field type. + int type; + Class<?> fieldType = field.getType(); + if (fieldType == String.class) { + type = TYPE_STRING; + } else if (fieldType == boolean.class) { + type = TYPE_BOOLEAN; + } else if (fieldType == short.class) { + type = TYPE_SHORT; + } else if (fieldType == int.class) { + type = TYPE_INT; + } else if (fieldType == long.class) { + type = TYPE_LONG; + } else if (fieldType == float.class) { + type = TYPE_FLOAT; + } else if (fieldType == double.class) { + type = TYPE_DOUBLE; + } else if (fieldType == byte[].class) { + type = TYPE_BLOB; + } else { + throw new IllegalArgumentException( + "Unsupported field type for column: " + fieldType.getName()); + } + + // Add the column to the array. + int index = columns.size(); + columns.add(new ColumnInfo(info.value(), type, info.indexed(), + info.fullText(), info.defaultValue(), field, index)); + } + } + + public static final class ColumnInfo { + private static final String ID_KEY = "_id"; + + public final String name; + public final int type; + public final boolean indexed; + public final boolean fullText; + public final String defaultValue; + public final Field field; + public final int projectionIndex; + + public ColumnInfo(String name, int type, boolean indexed, + boolean fullText, String defaultValue, Field field, int projectionIndex) { + this.name = name.toLowerCase(); + this.type = type; + this.indexed = indexed; + this.fullText = fullText; + this.defaultValue = defaultValue; + this.field = field; + this.projectionIndex = projectionIndex; + + field.setAccessible(true); // in order to set non-public fields + } + + public boolean isId() { + return ID_KEY.equals(name); + } + } +} diff --git a/gallerycommon/src/com/android/gallery3d/common/FileCache.java b/gallerycommon/src/com/android/gallery3d/common/FileCache.java new file mode 100644 index 000000000..a69d6e170 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/FileCache.java @@ -0,0 +1,303 @@ +/* + * 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.common; + +import com.android.gallery3d.common.Entry.Table; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +import java.io.File; +import java.io.IOException; + +public class FileCache { + private static final int LRU_CAPACITY = 4; + private static final int MAX_DELETE_COUNT = 16; + + private static final String TAG = "FileCache"; + private static final String TABLE_NAME = FileEntry.SCHEMA.getTableName(); + private static final String FILE_PREFIX = "download"; + private static final String FILE_POSTFIX = ".tmp"; + + private static final String QUERY_WHERE = + FileEntry.Columns.HASH_CODE + "=? AND " + FileEntry.Columns.CONTENT_URL + "=?"; + private static final String ID_WHERE = FileEntry.Columns.ID + "=?"; + private static final String[] PROJECTION_SIZE_SUM = + {String.format("sum(%s)", FileEntry.Columns.SIZE)}; + private static final String FREESPACE_PROJECTION[] = { + FileEntry.Columns.ID, FileEntry.Columns.FILENAME, + FileEntry.Columns.CONTENT_URL, FileEntry.Columns.SIZE}; + private static final String FREESPACE_ORDER_BY = + String.format("%s ASC", FileEntry.Columns.LAST_ACCESS); + + private final LruCache<String, CacheEntry> mEntryMap = + new LruCache<String, CacheEntry>(LRU_CAPACITY); + + private File mRootDir; + private long mCapacity; + private boolean mInitialized = false; + private long mTotalBytes; + + private DatabaseHelper mDbHelper; + + public static final class CacheEntry { + private long id; + public String contentUrl; + public File cacheFile; + + private CacheEntry(long id, String contentUrl, File cacheFile) { + this.id = id; + this.contentUrl = contentUrl; + this.cacheFile = cacheFile; + } + } + + public static void deleteFiles(Context context, File rootDir, String dbName) { + try { + context.getDatabasePath(dbName).delete(); + File[] files = rootDir.listFiles(); + if (files == null) return; + for (File file : rootDir.listFiles()) { + String name = file.getName(); + if (file.isFile() && name.startsWith(FILE_PREFIX) + && name.endsWith(FILE_POSTFIX)) file.delete(); + } + } catch (Throwable t) { + Log.w(TAG, "cannot reset database", t); + } + } + + public FileCache(Context context, File rootDir, String dbName, long capacity) { + mRootDir = Utils.checkNotNull(rootDir); + mCapacity = capacity; + mDbHelper = new DatabaseHelper(context, dbName); + } + + public void store(String downloadUrl, File file) { + if (!mInitialized) initialize(); + + Utils.assertTrue(file.getParentFile().equals(mRootDir)); + FileEntry entry = new FileEntry(); + entry.hashCode = Utils.crc64Long(downloadUrl); + entry.contentUrl = downloadUrl; + entry.filename = file.getName(); + entry.size = file.length(); + entry.lastAccess = System.currentTimeMillis(); + if (entry.size >= mCapacity) { + file.delete(); + throw new IllegalArgumentException("file too large: " + entry.size); + } + synchronized (this) { + FileEntry original = queryDatabase(downloadUrl); + if (original != null) { + file.delete(); + entry.filename = original.filename; + entry.size = original.size; + } else { + mTotalBytes += entry.size; + } + FileEntry.SCHEMA.insertOrReplace( + mDbHelper.getWritableDatabase(), entry); + if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT); + } + } + + public CacheEntry lookup(String downloadUrl) { + if (!mInitialized) initialize(); + CacheEntry entry; + synchronized (mEntryMap) { + entry = mEntryMap.get(downloadUrl); + } + + if (entry != null) { + synchronized (this) { + updateLastAccess(entry.id); + } + return entry; + } + + synchronized (this) { + FileEntry file = queryDatabase(downloadUrl); + if (file == null) return null; + entry = new CacheEntry( + file.id, downloadUrl, new File(mRootDir, file.filename)); + if (!entry.cacheFile.isFile()) { // file has been removed + try { + mDbHelper.getWritableDatabase().delete( + TABLE_NAME, ID_WHERE, new String[] {String.valueOf(file.id)}); + mTotalBytes -= file.size; + } catch (Throwable t) { + Log.w(TAG, "cannot delete entry: " + file.filename, t); + } + return null; + } + synchronized (mEntryMap) { + mEntryMap.put(downloadUrl, entry); + } + return entry; + } + } + + private FileEntry queryDatabase(String downloadUrl) { + long hash = Utils.crc64Long(downloadUrl); + String whereArgs[] = new String[] {String.valueOf(hash), downloadUrl}; + Cursor cursor = mDbHelper.getReadableDatabase().query(TABLE_NAME, + FileEntry.SCHEMA.getProjection(), + QUERY_WHERE, whereArgs, null, null, null); + try { + if (!cursor.moveToNext()) return null; + FileEntry entry = new FileEntry(); + FileEntry.SCHEMA.cursorToObject(cursor, entry); + updateLastAccess(entry.id); + return entry; + } finally { + cursor.close(); + } + } + + private void updateLastAccess(long id) { + ContentValues values = new ContentValues(); + values.put(FileEntry.Columns.LAST_ACCESS, System.currentTimeMillis()); + mDbHelper.getWritableDatabase().update(TABLE_NAME, + values, ID_WHERE, new String[] {String.valueOf(id)}); + } + + public File createFile() throws IOException { + return File.createTempFile(FILE_PREFIX, FILE_POSTFIX, mRootDir); + } + + private synchronized void initialize() { + if (mInitialized) return; + mInitialized = true; + + if (!mRootDir.isDirectory()) { + mRootDir.mkdirs(); + if (!mRootDir.isDirectory()) { + throw new RuntimeException("cannot create: " + mRootDir.getAbsolutePath()); + } + } + + Cursor cursor = mDbHelper.getReadableDatabase().query( + TABLE_NAME, PROJECTION_SIZE_SUM, + null, null, null, null, null); + try { + if (cursor.moveToNext()) mTotalBytes = cursor.getLong(0); + } finally { + cursor.close(); + } + if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT); + } + + private void freeSomeSpaceIfNeed(int maxDeleteFileCount) { + Cursor cursor = mDbHelper.getReadableDatabase().query( + TABLE_NAME, FREESPACE_PROJECTION, + null, null, null, null, FREESPACE_ORDER_BY); + try { + while (maxDeleteFileCount > 0 + && mTotalBytes > mCapacity && cursor.moveToNext()) { + long id = cursor.getLong(0); + String path = cursor.getString(1); + String url = cursor.getString(2); + long size = cursor.getLong(3); + + synchronized (mEntryMap) { + // if some one still uses it + if (mEntryMap.containsKey(url)) continue; + } + + --maxDeleteFileCount; + if (new File(mRootDir, path).delete()) { + mTotalBytes -= size; + mDbHelper.getWritableDatabase().delete(TABLE_NAME, + ID_WHERE, new String[]{String.valueOf(id)}); + } else { + Log.w(TAG, "unable to delete file: " + path); + } + } + } finally { + cursor.close(); + } + } + + @Table("files") + private static class FileEntry extends Entry { + public static final EntrySchema SCHEMA = new EntrySchema(FileEntry.class); + + public interface Columns extends Entry.Columns { + public static final String HASH_CODE = "hash_code"; + public static final String CONTENT_URL = "content_url"; + public static final String FILENAME = "filename"; + public static final String SIZE = "size"; + public static final String LAST_ACCESS = "last_access"; + } + + @Column(value = Columns.HASH_CODE, indexed = true) + public long hashCode; + + @Column(Columns.CONTENT_URL) + public String contentUrl; + + @Column(Columns.FILENAME) + public String filename; + + @Column(Columns.SIZE) + public long size; + + @Column(value = Columns.LAST_ACCESS, indexed = true) + public long lastAccess; + + @Override + public String toString() { + return new StringBuilder() + .append("hash_code: ").append(hashCode).append(", ") + .append("content_url").append(contentUrl).append(", ") + .append("last_access").append(lastAccess).append(", ") + .append("filename").append(filename).toString(); + } + } + + private final class DatabaseHelper extends SQLiteOpenHelper { + public static final int DATABASE_VERSION = 1; + + public DatabaseHelper(Context context, String dbName) { + super(context, dbName, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + FileEntry.SCHEMA.createTables(db); + + // delete old files + for (File file : mRootDir.listFiles()) { + if (!file.delete()) { + Log.w(TAG, "fail to remove: " + file.getAbsolutePath()); + } + } + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + //reset everything + FileEntry.SCHEMA.dropTables(db); + onCreate(db); + } + } +} diff --git a/gallerycommon/src/com/android/gallery3d/common/Fingerprint.java b/gallerycommon/src/com/android/gallery3d/common/Fingerprint.java new file mode 100644 index 000000000..39fcf9e09 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/Fingerprint.java @@ -0,0 +1,191 @@ +/* + * 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.common; + +import android.content.ContentResolver; +import android.net.Uri; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.List; + +/** + * MD5-based digest Wrapper. + */ +public class Fingerprint { + // Instance of the MessageDigest using our specified digest algorithm. + private static final MessageDigest DIGESTER; + + /** + * Name of the digest algorithm we use in {@link java.security.MessageDigest} + */ + private static final String DIGEST_MD5 = "md5"; + + // Version 1 streamId prefix. + // Hard coded stream id length limit is 40-chars. Don't ask! + private static final String STREAM_ID_CS_PREFIX = "cs_01_"; + + // 16 bytes for 128-bit fingerprint + private static final int FINGERPRINT_BYTE_LENGTH; + + // length of prefix + 32 hex chars for 128-bit fingerprint + private static final int STREAM_ID_CS_01_LENGTH; + + static { + try { + DIGESTER = MessageDigest.getInstance(DIGEST_MD5); + FINGERPRINT_BYTE_LENGTH = DIGESTER.getDigestLength(); + STREAM_ID_CS_01_LENGTH = STREAM_ID_CS_PREFIX.length() + + (FINGERPRINT_BYTE_LENGTH * 2); + } catch (NoSuchAlgorithmException e) { + // can't continue, but really shouldn't happen + throw new IllegalStateException(e); + } + } + + // md5 digest bytes. + private final byte[] mMd5Digest; + + /** + * Creates a new Fingerprint. + */ + public Fingerprint(byte[] bytes) { + if ((bytes == null) || (bytes.length != FINGERPRINT_BYTE_LENGTH)) { + throw new IllegalArgumentException(); + } + mMd5Digest = bytes; + } + + /** + * Creates a Fingerprint based on the contents of a file. + * + * Note that this will close() stream after calculating the digest. + * @param byteCount length of original data will be stored at byteCount[0] as a side product + * of the fingerprint calculation + */ + public static Fingerprint fromInputStream(InputStream stream, long[] byteCount) + throws IOException { + DigestInputStream in = null; + long count = 0; + try { + in = new DigestInputStream(stream, DIGESTER); + byte[] bytes = new byte[8192]; + while (true) { + // scan through file to compute a fingerprint. + int n = in.read(bytes); + if (n < 0) break; + count += n; + } + } finally { + if (in != null) in.close(); + } + if ((byteCount != null) && (byteCount.length > 0)) byteCount[0] = count; + return new Fingerprint(in.getMessageDigest().digest()); + } + + /** + * Decodes a string stream id to a 128-bit fingerprint. + */ + public static Fingerprint fromStreamId(String streamId) { + if ((streamId == null) + || !streamId.startsWith(STREAM_ID_CS_PREFIX) + || (streamId.length() != STREAM_ID_CS_01_LENGTH)) { + throw new IllegalArgumentException("bad streamId: " + streamId); + } + + // decode the hex bytes of the fingerprint portion + byte[] bytes = new byte[FINGERPRINT_BYTE_LENGTH]; + int byteIdx = 0; + for (int idx = STREAM_ID_CS_PREFIX.length(); idx < STREAM_ID_CS_01_LENGTH; + idx += 2) { + int value = (toDigit(streamId, idx) << 4) | toDigit(streamId, idx + 1); + bytes[byteIdx++] = (byte) (value & 0xff); + } + return new Fingerprint(bytes); + } + + /** + * Scans a list of strings for a valid streamId. + * + * @param streamIdList list of stream id's to be scanned + * @return valid fingerprint or null if it can't be found + */ + public static Fingerprint extractFingerprint(List<String> streamIdList) { + for (String streamId : streamIdList) { + if (streamId.startsWith(STREAM_ID_CS_PREFIX)) { + return fromStreamId(streamId); + } + } + return null; + } + + /** + * Encodes a 128-bit fingerprint as a string stream id. + * + * Stream id string is limited to 40 characters, which could be digits, lower case ASCII and + * underscores. + */ + public String toStreamId() { + StringBuilder streamId = new StringBuilder(STREAM_ID_CS_PREFIX); + appendHexFingerprint(streamId, mMd5Digest); + return streamId.toString(); + } + + public byte[] getBytes() { + return mMd5Digest; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Fingerprint)) return false; + Fingerprint other = (Fingerprint) obj; + return Arrays.equals(mMd5Digest, other.mMd5Digest); + } + + public boolean equals(byte[] md5Digest) { + return Arrays.equals(mMd5Digest, md5Digest); + } + + @Override + public int hashCode() { + return Arrays.hashCode(mMd5Digest); + } + + // Utility methods. + + private static int toDigit(String streamId, int index) { + int digit = Character.digit(streamId.charAt(index), 16); + if (digit < 0) { + throw new IllegalArgumentException("illegal hex digit in " + streamId); + } + return digit; + } + + private static void appendHexFingerprint(StringBuilder sb, byte[] bytes) { + for (int idx = 0; idx < FINGERPRINT_BYTE_LENGTH; idx++) { + int value = bytes[idx]; + sb.append(Integer.toHexString((value >> 4) & 0x0f)); + sb.append(Integer.toHexString(value& 0x0f)); + } + } +} diff --git a/gallerycommon/src/com/android/gallery3d/common/HttpClientFactory.java b/gallerycommon/src/com/android/gallery3d/common/HttpClientFactory.java new file mode 100644 index 000000000..cb95e3329 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/HttpClientFactory.java @@ -0,0 +1,134 @@ +/* + * 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.common; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Build; +import android.util.Log; + +import org.apache.http.HttpVersion; +import org.apache.http.client.HttpClient; +import org.apache.http.conn.params.ConnManagerParams; +import org.apache.http.params.CoreProtocolPNames; +import org.apache.http.params.HttpParams; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Constructs {@link HttpClient} instances and isolates client code from API + * level differences. + */ +public final class HttpClientFactory { + // TODO: migrate GDataClient to use this util method instead of apache's + // DefaultHttpClient. + /** + * Creates an HttpClient with the userAgent string constructed from the + * package name contained in the context. + * @return the client + */ + public static HttpClient newHttpClient(Context context) { + return HttpClientFactory.newHttpClient(getUserAgent(context)); + } + + /** + * Creates an HttpClient with the specified userAgent string. + * @param userAgent the userAgent string + * @return the client + */ + public static HttpClient newHttpClient(String userAgent) { + // AndroidHttpClient is available on all platform releases, + // but is hidden until API Level 8 + try { + Class<?> clazz = Class.forName("android.net.http.AndroidHttpClient"); + Method newInstance = clazz.getMethod("newInstance", String.class); + Object instance = newInstance.invoke(null, userAgent); + + HttpClient client = (HttpClient) instance; + + // ensure we default to HTTP 1.1 + HttpParams params = client.getParams(); + params.setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_1); + + // AndroidHttpClient sets these two parameters thusly by default: + // HttpConnectionParams.setSoTimeout(params, 60 * 1000); + // HttpConnectionParams.setConnectionTimeout(params, 60 * 1000); + + // however it doesn't set this one... + ConnManagerParams.setTimeout(params, 60 * 1000); + + return client; + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + /** + * Closes an HttpClient. + */ + public static void close(HttpClient client) { + // AndroidHttpClient is available on all platform releases, + // but is hidden until API Level 8 + try { + Class<?> clazz = client.getClass(); + Method method = clazz.getMethod("close", (Class<?>[]) null); + method.invoke(client, (Object[]) null); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private static String sUserAgent = null; + + private static String getUserAgent(Context context) { + if (sUserAgent == null) { + PackageInfo pi; + try { + pi = context.getPackageManager().getPackageInfo( + context.getPackageName(), 0); + } catch (NameNotFoundException e) { + throw new IllegalStateException("getPackageInfo failed"); + } + sUserAgent = String.format("%s/%s; %s/%s/%s/%s; %s/%s/%s", + pi.packageName, + pi.versionName, + Build.BRAND, + Build.DEVICE, + Build.MODEL, + Build.ID, + Build.VERSION.SDK, + Build.VERSION.RELEASE, + Build.VERSION.INCREMENTAL); + } + return sUserAgent; + } + + private HttpClientFactory() { + } +} diff --git a/gallerycommon/src/com/android/gallery3d/common/LruCache.java b/gallerycommon/src/com/android/gallery3d/common/LruCache.java new file mode 100644 index 000000000..81dabf773 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/LruCache.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2009 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.common; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * An LRU cache which stores recently inserted entries and all entries ever + * inserted which still has a strong reference elsewhere. + */ +public class LruCache<K, V> { + + private final HashMap<K, V> mLruMap; + private final HashMap<K, Entry<K, V>> mWeakMap = + new HashMap<K, Entry<K, V>>(); + private ReferenceQueue<V> mQueue = new ReferenceQueue<V>(); + + @SuppressWarnings("serial") + public LruCache(final int capacity) { + mLruMap = new LinkedHashMap<K, V>(16, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { + return size() > capacity; + } + }; + } + + 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; + } + } + + @SuppressWarnings("unchecked") + 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 boolean containsKey(K key) { + cleanUpWeakMap(); + return mWeakMap.containsKey(key); + } + + public synchronized V put(K key, V value) { + cleanUpWeakMap(); + mLruMap.put(key, value); + 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(); + V value = mLruMap.get(key); + if (value != null) return value; + Entry<K, V> entry = mWeakMap.get(key); + return entry == null ? null : entry.get(); + } + + public synchronized void clear() { + mLruMap.clear(); + mWeakMap.clear(); + mQueue = new ReferenceQueue<V>(); + } +} diff --git a/gallerycommon/src/com/android/gallery3d/common/Utils.java b/gallerycommon/src/com/android/gallery3d/common/Utils.java new file mode 100644 index 000000000..efe2be213 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/Utils.java @@ -0,0 +1,407 @@ +/* + * 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.common; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.database.Cursor; +import android.os.Build; +import android.os.Environment; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.StatFs; +import android.text.TextUtils; +import android.util.Log; + +import java.io.Closeable; +import java.io.InterruptedIOException; +import java.util.Random; + +public class Utils { + private static final String TAG = "Utils"; + private static final String DEBUG_TAG = "GalleryDebug"; + + private static final long POLY64REV = 0x95AC9329AC4BC9B5L; + private static final long INITIALCRC = 0xFFFFFFFFFFFFFFFFL; + + private static long[] sCrcTable = new long[256]; + + // Throws AssertionError if the input is false. + public static void assertTrue(boolean cond) { + if (!cond) { + throw new AssertionError(); + } + } + + // Throws AssertionError if the input is false. + public static void assertTrue(boolean cond, String message, Object ... args) { + if (!cond) { + throw new AssertionError( + args.length == 0 ? message : String.format(message, args)); + } + } + + // Throws NullPointerException if the input is null. + public static <T> T checkNotNull(T object) { + if (object == null) throw new NullPointerException(); + return object; + } + + // Returns true if two input Object are both null or equal + // to each other. + public static boolean equals(Object a, Object b) { + return (a == b) || (a == null ? false : a.equals(b)); + } + + // Returns true if the input is power of 2. + // Throws IllegalArgumentException if the input is <= 0. + public static boolean isPowerOf2(int n) { + if (n <= 0) throw new IllegalArgumentException(); + return (n & -n) == n; + } + + // Returns the next power of two. + // Returns the input if it is already power of 2. + // Throws IllegalArgumentException if the input is <= 0 or + // the answer overflows. + public static int nextPowerOf2(int n) { + if (n <= 0 || n > (1 << 30)) throw new IllegalArgumentException(); + n -= 1; + n |= n >> 16; + n |= n >> 8; + n |= n >> 4; + n |= n >> 2; + n |= n >> 1; + return n + 1; + } + + // Returns the previous power of two. + // Returns the input if it is already power of 2. + // Throws IllegalArgumentException if the input is <= 0 + public static int prevPowerOf2(int n) { + if (n <= 0) throw new IllegalArgumentException(); + return Integer.highestOneBit(n); + } + + // Returns the euclidean distance between (x, y) and (sx, sy). + public static float distance(float x, float y, float sx, float sy) { + float dx = x - sx; + float dy = y - sy; + return (float) Math.hypot(dx, dy); + } + + // Returns the input value x clamped to the range [min, max]. + public static int clamp(int x, int min, int max) { + if (x > max) return max; + if (x < min) return min; + return x; + } + + // Returns the input value x clamped to the range [min, max]. + public static float clamp(float x, float min, float max) { + if (x > max) return max; + if (x < min) return min; + return x; + } + + // Returns the input value x clamped to the range [min, max]. + public static long clamp(long x, long min, long max) { + if (x > max) return max; + if (x < min) return min; + return x; + } + + public static boolean isOpaque(int color) { + return color >>> 24 == 0xFF; + } + + public static <T> void swap(T[] array, int i, int j) { + T temp = array[i]; + array[i] = array[j]; + array[j] = temp; + } + + public static void swap(int[] array, int i, int j) { + int temp = array[i]; + array[i] = array[j]; + array[j] = temp; + } + + /** + * A function thats returns a 64-bit crc for string + * + * @param in input string + * @return a 64-bit crc value + */ + public static final long crc64Long(String in) { + if (in == null || in.length() == 0) { + return 0; + } + return crc64Long(getBytes(in)); + } + + static { + // http://bioinf.cs.ucl.ac.uk/downloads/crc64/crc64.c + long part; + for (int i = 0; i < 256; i++) { + part = i; + for (int j = 0; j < 8; j++) { + long x = ((int) part & 1) != 0 ? POLY64REV : 0; + part = (part >> 1) ^ x; + } + sCrcTable[i] = part; + } + } + + public static final long crc64Long(byte[] buffer) { + long crc = INITIALCRC; + for (int k = 0, n = buffer.length; k < n; ++k) { + crc = sCrcTable[(((int) crc) ^ buffer[k]) & 0xff] ^ (crc >> 8); + } + return crc; + } + + 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; + } + + public static void closeSilently(Closeable c) { + if (c == null) return; + try { + c.close(); + } catch (Throwable t) { + Log.w(TAG, "close fail", t); + } + } + + public static int compare(long a, long b) { + return a < b ? -1 : a == b ? 0 : 1; + } + + public static int ceilLog2(float value) { + int i; + for (i = 0; i < 31; i++) { + if ((1 << i) >= value) break; + } + return i; + } + + public static int floorLog2(float value) { + int i; + for (i = 0; i < 31; i++) { + if ((1 << i) > value) break; + } + return i - 1; + } + + public static void closeSilently(ParcelFileDescriptor fd) { + try { + if (fd != null) fd.close(); + } catch (Throwable t) { + Log.w(TAG, "fail to close", t); + } + } + + public static void closeSilently(Cursor cursor) { + try { + if (cursor != null) cursor.close(); + } catch (Throwable t) { + Log.w(TAG, "fail to close", t); + } + } + + public static float interpolateAngle( + float source, float target, float progress) { + // interpolate the angle from source to target + // We make the difference in the range of [-179, 180], this is the + // shortest path to change source to target. + float diff = target - source; + if (diff < 0) diff += 360f; + if (diff > 180) diff -= 360f; + + float result = source + diff * progress; + return result < 0 ? result + 360f : result; + } + + public static float interpolateScale( + float source, float target, float progress) { + return source + progress * (target - source); + } + + public static String ensureNotNull(String value) { + return value == null ? "" : value; + } + + // Used for debugging. Should be removed before submitting. + public static void debug(String format, Object ... args) { + if (args.length == 0) { + Log.d(DEBUG_TAG, format); + } else { + Log.d(DEBUG_TAG, String.format(format, args)); + } + } + + public static float parseFloatSafely(String content, float defaultValue) { + if (content == null) return defaultValue; + try { + return Float.parseFloat(content); + } catch (NumberFormatException e) { + Log.w(TAG, "invalid float: " + content, e); + return defaultValue; + } + } + + public static int parseIntSafely(String content, int defaultValue) { + if (content == null) return defaultValue; + try { + return Integer.parseInt(content); + } catch (NumberFormatException e) { + Log.w(TAG, "invalid int: " + content, e); + return defaultValue; + } + } + + public static boolean isNullOrEmpty(String exifMake) { + return TextUtils.isEmpty(exifMake); + } + + 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 void waitWithoutInterrupt(Object object) { + try { + object.wait(); + } catch (InterruptedException e) { + Log.w(TAG, "unexpected interrupt: " + object); + } + } + + public static void shuffle(int array[], Random random) { + for (int i = array.length; i > 0; --i) { + int t = random.nextInt(i); + if (t == i - 1) continue; + int tmp = array[i - 1]; + array[i - 1] = array[t]; + array[t] = tmp; + } + } + + public static boolean handleInterrruptedException(Throwable e) { + // A helper to deal with the interrupt exception + // If an interrupt detected, we will setup the bit again. + if (e instanceof InterruptedIOException + || e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + return true; + } + return false; + } + + /** + * @return String with special XML characters escaped. + */ + public static String escapeXml(String s) { + StringBuilder sb = new StringBuilder(); + for (int i = 0, len = s.length(); i < len; ++i) { + char c = s.charAt(i); + switch (c) { + case '<': sb.append("<"); break; + case '>': sb.append(">"); break; + case '\"': sb.append("""); break; + case '\'': sb.append("'"); break; + case '&': sb.append("&"); break; + default: sb.append(c); + } + } + return sb.toString(); + } + + public static String getUserAgent(Context context) { + PackageInfo packageInfo; + try { + packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + } catch (NameNotFoundException e) { + throw new IllegalStateException("getPackageInfo failed"); + } + return String.format("%s/%s; %s/%s/%s/%s; %s/%s/%s", + packageInfo.packageName, + packageInfo.versionName, + Build.BRAND, + Build.DEVICE, + Build.MODEL, + Build.ID, + Build.VERSION.SDK, + Build.VERSION.RELEASE, + Build.VERSION.INCREMENTAL); + } + + public static String[] copyOf(String[] source, int newSize) { + String[] result = new String[newSize]; + newSize = Math.min(source.length, newSize); + System.arraycopy(source, 0, result, 0, newSize); + return result; + } + + public static PendingIntent deserializePendingIntent(byte[] rawPendingIntent) { + Parcel parcel = null; + try { + if (rawPendingIntent != null) { + parcel = Parcel.obtain(); + parcel.unmarshall(rawPendingIntent, 0, rawPendingIntent.length); + return PendingIntent.readPendingIntentOrNullFromParcel(parcel); + } else { + return null; + } + } catch (Exception e) { + throw new IllegalArgumentException("error parsing PendingIntent"); + } finally { + if (parcel != null) parcel.recycle(); + } + } + + public static byte[] serializePendingIntent(PendingIntent pendingIntent) { + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + PendingIntent.writePendingIntentOrNullToParcel(pendingIntent, parcel); + return parcel.marshall(); + } finally { + if (parcel != null) parcel.recycle(); + } + } +} diff --git a/proguard.flags b/proguard.flags new file mode 100644 index 000000000..0df05e3dc --- /dev/null +++ b/proguard.flags @@ -0,0 +1,7 @@ +# Keep all classes extended from com.android.gallery3d.common.Entry +# Since we annotate on the fields and use reflection to create SQL +# according to those field. + +-keep class * extends com.android.gallery3d.common.Entry { + @com.android.gallery3d.common.Entry$Column <fields>; +} diff --git a/res/drawable-hdpi/actionbar_translucent.9.png b/res/drawable-hdpi/actionbar_translucent.9.png Binary files differnew file mode 100644 index 000000000..f18761f04 --- /dev/null +++ b/res/drawable-hdpi/actionbar_translucent.9.png diff --git a/res/drawable-hdpi/album_frame.9.png b/res/drawable-hdpi/album_frame.9.png Binary files differnew file mode 100644 index 000000000..c9eb35fb0 --- /dev/null +++ b/res/drawable-hdpi/album_frame.9.png diff --git a/res/drawable-hdpi/appwidget_photo_border.9.png b/res/drawable-hdpi/appwidget_photo_border.9.png Binary files differnew file mode 100644 index 000000000..2d5fd62a7 --- /dev/null +++ b/res/drawable-hdpi/appwidget_photo_border.9.png diff --git a/res/drawable-hdpi/background.jpg b/res/drawable-hdpi/background.jpg Binary files differnew file mode 100644 index 000000000..42b74c5e3 --- /dev/null +++ b/res/drawable-hdpi/background.jpg diff --git a/res/drawable-hdpi/background_portrait.jpg b/res/drawable-hdpi/background_portrait.jpg Binary files differnew file mode 100644 index 000000000..75309b486 --- /dev/null +++ b/res/drawable-hdpi/background_portrait.jpg diff --git a/res/drawable-hdpi/border_photo_frame_widget_focused_holo.9.png b/res/drawable-hdpi/border_photo_frame_widget_focused_holo.9.png Binary files differnew file mode 100644 index 000000000..1cb157ea6 --- /dev/null +++ b/res/drawable-hdpi/border_photo_frame_widget_focused_holo.9.png diff --git a/res/drawable-hdpi/border_photo_frame_widget_focused_pressed_holo.9.png b/res/drawable-hdpi/border_photo_frame_widget_focused_pressed_holo.9.png Binary files differnew file mode 100644 index 000000000..340cdcc71 --- /dev/null +++ b/res/drawable-hdpi/border_photo_frame_widget_focused_pressed_holo.9.png diff --git a/res/drawable-hdpi/border_photo_frame_widget_holo.9.png b/res/drawable-hdpi/border_photo_frame_widget_holo.9.png Binary files differnew file mode 100644 index 000000000..dc7092bce --- /dev/null +++ b/res/drawable-hdpi/border_photo_frame_widget_holo.9.png diff --git a/res/drawable-hdpi/border_photo_frame_widget_pressed_holo.9.png b/res/drawable-hdpi/border_photo_frame_widget_pressed_holo.9.png Binary files differnew file mode 100644 index 000000000..86d4cf1a8 --- /dev/null +++ b/res/drawable-hdpi/border_photo_frame_widget_pressed_holo.9.png diff --git a/res/drawable-hdpi/btn_default_normal_holo_dark.9.png b/res/drawable-hdpi/btn_default_normal_holo_dark.9.png Binary files differnew file mode 100644 index 000000000..d608e44ef --- /dev/null +++ b/res/drawable-hdpi/btn_default_normal_holo_dark.9.png diff --git a/res/drawable-hdpi/btn_default_pressed_holo_dark.9.png b/res/drawable-hdpi/btn_default_pressed_holo_dark.9.png Binary files differnew file mode 100644 index 000000000..40957ee83 --- /dev/null +++ b/res/drawable-hdpi/btn_default_pressed_holo_dark.9.png diff --git a/res/drawable-hdpi/btn_make_offline_disabled_on_holo_dark.png b/res/drawable-hdpi/btn_make_offline_disabled_on_holo_dark.png Binary files differnew file mode 100644 index 000000000..1c913ddcf --- /dev/null +++ b/res/drawable-hdpi/btn_make_offline_disabled_on_holo_dark.png diff --git a/res/drawable-hdpi/btn_make_offline_normal_off_holo_dark.png b/res/drawable-hdpi/btn_make_offline_normal_off_holo_dark.png Binary files differnew file mode 100644 index 000000000..a3605882f --- /dev/null +++ b/res/drawable-hdpi/btn_make_offline_normal_off_holo_dark.png diff --git a/res/drawable-hdpi/btn_make_offline_normal_on_holo_dark.png b/res/drawable-hdpi/btn_make_offline_normal_on_holo_dark.png Binary files differnew file mode 100644 index 000000000..41c797862 --- /dev/null +++ b/res/drawable-hdpi/btn_make_offline_normal_on_holo_dark.png diff --git a/res/drawable-hdpi/cab_divider_vertical_dark.png b/res/drawable-hdpi/cab_divider_vertical_dark.png Binary files differnew file mode 100644 index 000000000..f7ed6dff8 --- /dev/null +++ b/res/drawable-hdpi/cab_divider_vertical_dark.png diff --git a/res/drawable-hdpi/camera_crop_height_holo.png b/res/drawable-hdpi/camera_crop_height_holo.png Binary files differnew file mode 100644 index 000000000..19fdb8704 --- /dev/null +++ b/res/drawable-hdpi/camera_crop_height_holo.png diff --git a/res/drawable-hdpi/camera_crop_width_holo.png b/res/drawable-hdpi/camera_crop_width_holo.png Binary files differnew file mode 100644 index 000000000..3c82e11d0 --- /dev/null +++ b/res/drawable-hdpi/camera_crop_width_holo.png diff --git a/res/drawable-hdpi/dropdown_normal_holo_dark.9.png b/res/drawable-hdpi/dropdown_normal_holo_dark.9.png Binary files differnew file mode 100644 index 000000000..55250257a --- /dev/null +++ b/res/drawable-hdpi/dropdown_normal_holo_dark.9.png diff --git a/res/drawable-hdpi/focus_box.9.png b/res/drawable-hdpi/focus_box.9.png Binary files differnew file mode 100644 index 000000000..a1286f1cf --- /dev/null +++ b/res/drawable-hdpi/focus_box.9.png diff --git a/res/drawable-hdpi/gallery_widget_preview.png b/res/drawable-hdpi/gallery_widget_preview.png Binary files differnew file mode 100644 index 000000000..ff55bd58e --- /dev/null +++ b/res/drawable-hdpi/gallery_widget_preview.png diff --git a/res/drawable-hdpi/grid_selected.9.png b/res/drawable-hdpi/grid_selected.9.png Binary files differnew file mode 100644 index 000000000..383526e12 --- /dev/null +++ b/res/drawable-hdpi/grid_selected.9.png diff --git a/res/drawable-hdpi/grid_selected_top.9.png b/res/drawable-hdpi/grid_selected_top.9.png Binary files differnew file mode 100644 index 000000000..bacc00f60 --- /dev/null +++ b/res/drawable-hdpi/grid_selected_top.9.png diff --git a/res/drawable-hdpi/ic_album_overlay_camera_holo.png b/res/drawable-hdpi/ic_album_overlay_camera_holo.png Binary files differnew file mode 100644 index 000000000..189aa3d37 --- /dev/null +++ b/res/drawable-hdpi/ic_album_overlay_camera_holo.png diff --git a/res/drawable-hdpi/ic_album_overlay_folder_holo.png b/res/drawable-hdpi/ic_album_overlay_folder_holo.png Binary files differnew file mode 100644 index 000000000..469b22225 --- /dev/null +++ b/res/drawable-hdpi/ic_album_overlay_folder_holo.png diff --git a/res/drawable-hdpi/ic_album_overlay_picassa_holo.png b/res/drawable-hdpi/ic_album_overlay_picassa_holo.png Binary files differnew file mode 100644 index 000000000..7cdf8e4ec --- /dev/null +++ b/res/drawable-hdpi/ic_album_overlay_picassa_holo.png diff --git a/res/drawable-hdpi/ic_album_overlay_ptp_holo.png b/res/drawable-hdpi/ic_album_overlay_ptp_holo.png Binary files differnew file mode 100644 index 000000000..b72584048 --- /dev/null +++ b/res/drawable-hdpi/ic_album_overlay_ptp_holo.png diff --git a/res/drawable-hdpi/ic_control_fail.png b/res/drawable-hdpi/ic_control_fail.png Binary files differnew file mode 100644 index 000000000..7cab70d26 --- /dev/null +++ b/res/drawable-hdpi/ic_control_fail.png diff --git a/res/drawable-hdpi/ic_control_play.png b/res/drawable-hdpi/ic_control_play.png Binary files differnew file mode 100644 index 000000000..5b1eacb2a --- /dev/null +++ b/res/drawable-hdpi/ic_control_play.png diff --git a/res/drawable-hdpi/ic_manage_pin.png b/res/drawable-hdpi/ic_manage_pin.png Binary files differnew file mode 100644 index 000000000..0b688709e --- /dev/null +++ b/res/drawable-hdpi/ic_manage_pin.png diff --git a/res/drawable-hdpi/ic_menu_camera_holo_light.png b/res/drawable-hdpi/ic_menu_camera_holo_light.png Binary files differnew file mode 100644 index 000000000..5f0f064e6 --- /dev/null +++ b/res/drawable-hdpi/ic_menu_camera_holo_light.png diff --git a/res/drawable-hdpi/ic_menu_cancel_holo_light.png b/res/drawable-hdpi/ic_menu_cancel_holo_light.png Binary files differnew file mode 100644 index 000000000..9338a5199 --- /dev/null +++ b/res/drawable-hdpi/ic_menu_cancel_holo_light.png diff --git a/res/drawable-hdpi/ic_menu_info_details.png b/res/drawable-hdpi/ic_menu_info_details.png Binary files differnew file mode 100644 index 000000000..2d1f7f324 --- /dev/null +++ b/res/drawable-hdpi/ic_menu_info_details.png diff --git a/res/drawable-hdpi/ic_menu_ptp_holo_light.png b/res/drawable-hdpi/ic_menu_ptp_holo_light.png Binary files differnew file mode 100644 index 000000000..5e80ce8fe --- /dev/null +++ b/res/drawable-hdpi/ic_menu_ptp_holo_light.png diff --git a/res/drawable-hdpi/ic_menu_save_holo_light.png b/res/drawable-hdpi/ic_menu_save_holo_light.png Binary files differnew file mode 100644 index 000000000..b13d2db4b --- /dev/null +++ b/res/drawable-hdpi/ic_menu_save_holo_light.png diff --git a/res/drawable-hdpi/ic_menu_share_holo_light.png b/res/drawable-hdpi/ic_menu_share_holo_light.png Binary files differnew file mode 100644 index 000000000..492d6090c --- /dev/null +++ b/res/drawable-hdpi/ic_menu_share_holo_light.png diff --git a/res/drawable-hdpi/ic_menu_slideshow_holo_light.png b/res/drawable-hdpi/ic_menu_slideshow_holo_light.png Binary files differnew file mode 100644 index 000000000..ca13dd887 --- /dev/null +++ b/res/drawable-hdpi/ic_menu_slideshow_holo_light.png diff --git a/res/drawable-hdpi/ic_menu_trash_holo_light.png b/res/drawable-hdpi/ic_menu_trash_holo_light.png Binary files differnew file mode 100644 index 000000000..721ee5ca2 --- /dev/null +++ b/res/drawable-hdpi/ic_menu_trash_holo_light.png diff --git a/res/drawable-hdpi/icn_media_pause_focused_holo_dark.png b/res/drawable-hdpi/icn_media_pause_focused_holo_dark.png Binary files differnew file mode 100644 index 000000000..6b4047bee --- /dev/null +++ b/res/drawable-hdpi/icn_media_pause_focused_holo_dark.png diff --git a/res/drawable-hdpi/icn_media_pause_normal_holo_dark.png b/res/drawable-hdpi/icn_media_pause_normal_holo_dark.png Binary files differnew file mode 100644 index 000000000..1945610de --- /dev/null +++ b/res/drawable-hdpi/icn_media_pause_normal_holo_dark.png diff --git a/res/drawable-hdpi/icn_media_pause_pressed_holo_dark.png b/res/drawable-hdpi/icn_media_pause_pressed_holo_dark.png Binary files differnew file mode 100644 index 000000000..3af461265 --- /dev/null +++ b/res/drawable-hdpi/icn_media_pause_pressed_holo_dark.png diff --git a/res/drawable-hdpi/icn_media_play_focused_holo_dark.png b/res/drawable-hdpi/icn_media_play_focused_holo_dark.png Binary files differnew file mode 100644 index 000000000..576f24797 --- /dev/null +++ b/res/drawable-hdpi/icn_media_play_focused_holo_dark.png diff --git a/res/drawable-hdpi/icn_media_play_normal_holo_dark.png b/res/drawable-hdpi/icn_media_play_normal_holo_dark.png Binary files differnew file mode 100644 index 000000000..16dd09dcd --- /dev/null +++ b/res/drawable-hdpi/icn_media_play_normal_holo_dark.png diff --git a/res/drawable-hdpi/icn_media_play_pressed_holo_dark.png b/res/drawable-hdpi/icn_media_play_pressed_holo_dark.png Binary files differnew file mode 100644 index 000000000..1bc4f796c --- /dev/null +++ b/res/drawable-hdpi/icn_media_play_pressed_holo_dark.png diff --git a/res/drawable-hdpi/import_translucent.9.png b/res/drawable-hdpi/import_translucent.9.png Binary files differnew file mode 100644 index 000000000..cb3152ba6 --- /dev/null +++ b/res/drawable-hdpi/import_translucent.9.png diff --git a/res/drawable-hdpi/manage_bar.9.png b/res/drawable-hdpi/manage_bar.9.png Binary files differnew file mode 100644 index 000000000..336c2d74d --- /dev/null +++ b/res/drawable-hdpi/manage_bar.9.png diff --git a/res/drawable-hdpi/manage_frame.9.png b/res/drawable-hdpi/manage_frame.9.png Binary files differnew file mode 100644 index 000000000..879c47ba1 --- /dev/null +++ b/res/drawable-hdpi/manage_frame.9.png diff --git a/res/drawable-hdpi/media_control_bg.9.png b/res/drawable-hdpi/media_control_bg.9.png Binary files differnew file mode 100644 index 000000000..afb7631b1 --- /dev/null +++ b/res/drawable-hdpi/media_control_bg.9.png diff --git a/res/drawable-hdpi/navstrip_translucent.9.png b/res/drawable-hdpi/navstrip_translucent.9.png Binary files differnew file mode 100644 index 000000000..5854af903 --- /dev/null +++ b/res/drawable-hdpi/navstrip_translucent.9.png diff --git a/res/drawable-hdpi/player_scrubber.png b/res/drawable-hdpi/player_scrubber.png Binary files differnew file mode 100644 index 000000000..426e3daff --- /dev/null +++ b/res/drawable-hdpi/player_scrubber.png diff --git a/res/drawable-hdpi/popup_full_dark.9.png b/res/drawable-hdpi/popup_full_dark.9.png Binary files differnew file mode 100644 index 000000000..2884abeab --- /dev/null +++ b/res/drawable-hdpi/popup_full_dark.9.png diff --git a/res/drawable-hdpi/preview.png b/res/drawable-hdpi/preview.png Binary files differnew file mode 100644 index 000000000..1f21a5be2 --- /dev/null +++ b/res/drawable-hdpi/preview.png diff --git a/res/drawable-hdpi/progress_bg_holo_dark.9.png b/res/drawable-hdpi/progress_bg_holo_dark.9.png Binary files differnew file mode 100644 index 000000000..5aea3d98a --- /dev/null +++ b/res/drawable-hdpi/progress_bg_holo_dark.9.png diff --git a/res/drawable-hdpi/progress_primary_holo_dark.9.png b/res/drawable-hdpi/progress_primary_holo_dark.9.png Binary files differnew file mode 100644 index 000000000..f134a5958 --- /dev/null +++ b/res/drawable-hdpi/progress_primary_holo_dark.9.png diff --git a/res/drawable-hdpi/progress_secondary_holo_dark.9.png b/res/drawable-hdpi/progress_secondary_holo_dark.9.png Binary files differnew file mode 100644 index 000000000..22d608ac7 --- /dev/null +++ b/res/drawable-hdpi/progress_secondary_holo_dark.9.png diff --git a/res/drawable-hdpi/scrollbar_handle_holo_dark.9.png b/res/drawable-hdpi/scrollbar_handle_holo_dark.9.png Binary files differnew file mode 100644 index 000000000..575edee14 --- /dev/null +++ b/res/drawable-hdpi/scrollbar_handle_holo_dark.9.png diff --git a/res/drawable-hdpi/spinner_76_inner_holo.png b/res/drawable-hdpi/spinner_76_inner_holo.png Binary files differnew file mode 100644 index 000000000..a1ef44c47 --- /dev/null +++ b/res/drawable-hdpi/spinner_76_inner_holo.png diff --git a/res/drawable-hdpi/spinner_76_outer_holo.png b/res/drawable-hdpi/spinner_76_outer_holo.png Binary files differnew file mode 100644 index 000000000..69e3ab76a --- /dev/null +++ b/res/drawable-hdpi/spinner_76_outer_holo.png diff --git a/res/drawable-hdpi/thumbnail_album_video_overlay_holo.png b/res/drawable-hdpi/thumbnail_album_video_overlay_holo.png Binary files differnew file mode 100644 index 000000000..567a69aaa --- /dev/null +++ b/res/drawable-hdpi/thumbnail_album_video_overlay_holo.png diff --git a/res/drawable-hdpi/videooverlay.png b/res/drawable-hdpi/videooverlay.png Binary files differnew file mode 100644 index 000000000..17188323d --- /dev/null +++ b/res/drawable-hdpi/videooverlay.png diff --git a/res/drawable-hdpi/wallpaper_picker_preview.png b/res/drawable-hdpi/wallpaper_picker_preview.png Binary files differnew file mode 100644 index 000000000..452b1251e --- /dev/null +++ b/res/drawable-hdpi/wallpaper_picker_preview.png diff --git a/res/drawable-mdpi/actionbar_translucent.9.png b/res/drawable-mdpi/actionbar_translucent.9.png Binary files differnew file mode 100644 index 000000000..f78fb8a76 --- /dev/null +++ b/res/drawable-mdpi/actionbar_translucent.9.png diff --git a/res/drawable-mdpi/album_frame.9.png b/res/drawable-mdpi/album_frame.9.png Binary files differnew file mode 100644 index 000000000..c9eb35fb0 --- /dev/null +++ b/res/drawable-mdpi/album_frame.9.png diff --git a/res/drawable-mdpi/appwidget_photo_border.9.png b/res/drawable-mdpi/appwidget_photo_border.9.png Binary files differnew file mode 100644 index 000000000..7c520fbf2 --- /dev/null +++ b/res/drawable-mdpi/appwidget_photo_border.9.png diff --git a/res/drawable-mdpi/background.jpg b/res/drawable-mdpi/background.jpg Binary files differnew file mode 100644 index 000000000..42b74c5e3 --- /dev/null +++ b/res/drawable-mdpi/background.jpg diff --git a/res/drawable-mdpi/background_portrait.jpg b/res/drawable-mdpi/background_portrait.jpg Binary files differnew file mode 100644 index 000000000..75309b486 --- /dev/null +++ b/res/drawable-mdpi/background_portrait.jpg diff --git a/res/drawable-mdpi/border_photo_frame_widget_focused_holo.9.png b/res/drawable-mdpi/border_photo_frame_widget_focused_holo.9.png Binary files differnew file mode 100644 index 000000000..89e2c5d58 --- /dev/null +++ b/res/drawable-mdpi/border_photo_frame_widget_focused_holo.9.png diff --git a/res/drawable-mdpi/border_photo_frame_widget_focused_pressed_holo.9.png b/res/drawable-mdpi/border_photo_frame_widget_focused_pressed_holo.9.png Binary files differnew file mode 100644 index 000000000..22e2fd160 --- /dev/null +++ b/res/drawable-mdpi/border_photo_frame_widget_focused_pressed_holo.9.png diff --git a/res/drawable-mdpi/border_photo_frame_widget_holo.9.png b/res/drawable-mdpi/border_photo_frame_widget_holo.9.png Binary files differnew file mode 100644 index 000000000..18d2cc81e --- /dev/null +++ b/res/drawable-mdpi/border_photo_frame_widget_holo.9.png diff --git a/res/drawable-mdpi/border_photo_frame_widget_pressed_holo.9.png b/res/drawable-mdpi/border_photo_frame_widget_pressed_holo.9.png Binary files differnew file mode 100644 index 000000000..4732b1227 --- /dev/null +++ b/res/drawable-mdpi/border_photo_frame_widget_pressed_holo.9.png diff --git a/res/drawable-mdpi/btn_default_normal_holo_dark.9.png b/res/drawable-mdpi/btn_default_normal_holo_dark.9.png Binary files differnew file mode 100644 index 000000000..d608e44ef --- /dev/null +++ b/res/drawable-mdpi/btn_default_normal_holo_dark.9.png diff --git a/res/drawable-mdpi/btn_default_pressed_holo_dark.9.png b/res/drawable-mdpi/btn_default_pressed_holo_dark.9.png Binary files differnew file mode 100644 index 000000000..40957ee83 --- /dev/null +++ b/res/drawable-mdpi/btn_default_pressed_holo_dark.9.png diff --git a/res/drawable-mdpi/btn_make_offline_disabled_on_holo_dark.png b/res/drawable-mdpi/btn_make_offline_disabled_on_holo_dark.png Binary files differnew file mode 100644 index 000000000..23305601b --- /dev/null +++ b/res/drawable-mdpi/btn_make_offline_disabled_on_holo_dark.png diff --git a/res/drawable-mdpi/btn_make_offline_normal_off_holo_dark.png b/res/drawable-mdpi/btn_make_offline_normal_off_holo_dark.png Binary files differnew file mode 100644 index 000000000..17c44f57f --- /dev/null +++ b/res/drawable-mdpi/btn_make_offline_normal_off_holo_dark.png diff --git a/res/drawable-mdpi/btn_make_offline_normal_on_holo_dark.png b/res/drawable-mdpi/btn_make_offline_normal_on_holo_dark.png Binary files differnew file mode 100644 index 000000000..f4ada23e9 --- /dev/null +++ b/res/drawable-mdpi/btn_make_offline_normal_on_holo_dark.png diff --git a/res/drawable-mdpi/cab_divider_vertical_dark.png b/res/drawable-mdpi/cab_divider_vertical_dark.png Binary files differnew file mode 100644 index 000000000..f7ed6dff8 --- /dev/null +++ b/res/drawable-mdpi/cab_divider_vertical_dark.png diff --git a/res/drawable-mdpi/camera_crop_height_holo.png b/res/drawable-mdpi/camera_crop_height_holo.png Binary files differnew file mode 100644 index 000000000..45a6da93a --- /dev/null +++ b/res/drawable-mdpi/camera_crop_height_holo.png diff --git a/res/drawable-mdpi/camera_crop_width_holo.png b/res/drawable-mdpi/camera_crop_width_holo.png Binary files differnew file mode 100644 index 000000000..e9f7a5c5c --- /dev/null +++ b/res/drawable-mdpi/camera_crop_width_holo.png diff --git a/res/drawable-mdpi/dropdown_normal_holo_dark.9.png b/res/drawable-mdpi/dropdown_normal_holo_dark.9.png Binary files differnew file mode 100644 index 000000000..55250257a --- /dev/null +++ b/res/drawable-mdpi/dropdown_normal_holo_dark.9.png diff --git a/res/drawable-mdpi/focus_box.9.png b/res/drawable-mdpi/focus_box.9.png Binary files differnew file mode 100644 index 000000000..a1286f1cf --- /dev/null +++ b/res/drawable-mdpi/focus_box.9.png diff --git a/res/drawable-mdpi/gallery_widget_preview.png b/res/drawable-mdpi/gallery_widget_preview.png Binary files differnew file mode 100644 index 000000000..ac51a4a0d --- /dev/null +++ b/res/drawable-mdpi/gallery_widget_preview.png diff --git a/res/drawable-mdpi/grid_selected.9.png b/res/drawable-mdpi/grid_selected.9.png Binary files differnew file mode 100644 index 000000000..383526e12 --- /dev/null +++ b/res/drawable-mdpi/grid_selected.9.png diff --git a/res/drawable-mdpi/grid_selected_top.9.png b/res/drawable-mdpi/grid_selected_top.9.png Binary files differnew file mode 100644 index 000000000..bacc00f60 --- /dev/null +++ b/res/drawable-mdpi/grid_selected_top.9.png diff --git a/res/drawable-mdpi/ic_album_overlay_camera_holo.png b/res/drawable-mdpi/ic_album_overlay_camera_holo.png Binary files differnew file mode 100644 index 000000000..15bdedfac --- /dev/null +++ b/res/drawable-mdpi/ic_album_overlay_camera_holo.png diff --git a/res/drawable-mdpi/ic_album_overlay_folder_holo.png b/res/drawable-mdpi/ic_album_overlay_folder_holo.png Binary files differnew file mode 100644 index 000000000..99b208837 --- /dev/null +++ b/res/drawable-mdpi/ic_album_overlay_folder_holo.png diff --git a/res/drawable-mdpi/ic_album_overlay_picassa_holo.png b/res/drawable-mdpi/ic_album_overlay_picassa_holo.png Binary files differnew file mode 100644 index 000000000..8e1e43272 --- /dev/null +++ b/res/drawable-mdpi/ic_album_overlay_picassa_holo.png diff --git a/res/drawable-mdpi/ic_album_overlay_ptp_holo.png b/res/drawable-mdpi/ic_album_overlay_ptp_holo.png Binary files differnew file mode 100644 index 000000000..adbd3d143 --- /dev/null +++ b/res/drawable-mdpi/ic_album_overlay_ptp_holo.png diff --git a/res/drawable-mdpi/ic_control_fail.png b/res/drawable-mdpi/ic_control_fail.png Binary files differnew file mode 100644 index 000000000..e572aecee --- /dev/null +++ b/res/drawable-mdpi/ic_control_fail.png diff --git a/res/drawable-mdpi/ic_control_play.png b/res/drawable-mdpi/ic_control_play.png Binary files differnew file mode 100644 index 000000000..2de5b4f61 --- /dev/null +++ b/res/drawable-mdpi/ic_control_play.png diff --git a/res/drawable-mdpi/ic_manage_pin.png b/res/drawable-mdpi/ic_manage_pin.png Binary files differnew file mode 100644 index 000000000..1324585b3 --- /dev/null +++ b/res/drawable-mdpi/ic_manage_pin.png diff --git a/res/drawable-mdpi/ic_menu_camera_holo_light.png b/res/drawable-mdpi/ic_menu_camera_holo_light.png Binary files differnew file mode 100644 index 000000000..d42508410 --- /dev/null +++ b/res/drawable-mdpi/ic_menu_camera_holo_light.png diff --git a/res/drawable-mdpi/ic_menu_cancel_holo_light.png b/res/drawable-mdpi/ic_menu_cancel_holo_light.png Binary files differnew file mode 100644 index 000000000..83776ba0e --- /dev/null +++ b/res/drawable-mdpi/ic_menu_cancel_holo_light.png diff --git a/res/drawable-mdpi/ic_menu_info_details.png b/res/drawable-mdpi/ic_menu_info_details.png Binary files differnew file mode 100644 index 000000000..8aca07d09 --- /dev/null +++ b/res/drawable-mdpi/ic_menu_info_details.png diff --git a/res/drawable-mdpi/ic_menu_ptp_holo_light.png b/res/drawable-mdpi/ic_menu_ptp_holo_light.png Binary files differnew file mode 100644 index 000000000..277a6202d --- /dev/null +++ b/res/drawable-mdpi/ic_menu_ptp_holo_light.png diff --git a/res/drawable-mdpi/ic_menu_save_holo_light.png b/res/drawable-mdpi/ic_menu_save_holo_light.png Binary files differnew file mode 100644 index 000000000..b2a33a29c --- /dev/null +++ b/res/drawable-mdpi/ic_menu_save_holo_light.png diff --git a/res/drawable-mdpi/ic_menu_share_holo_light.png b/res/drawable-mdpi/ic_menu_share_holo_light.png Binary files differnew file mode 100644 index 000000000..29574f5bd --- /dev/null +++ b/res/drawable-mdpi/ic_menu_share_holo_light.png diff --git a/res/drawable-mdpi/ic_menu_slideshow_holo_light.png b/res/drawable-mdpi/ic_menu_slideshow_holo_light.png Binary files differnew file mode 100644 index 000000000..a1affcf89 --- /dev/null +++ b/res/drawable-mdpi/ic_menu_slideshow_holo_light.png diff --git a/res/drawable-mdpi/ic_menu_trash_holo_light.png b/res/drawable-mdpi/ic_menu_trash_holo_light.png Binary files differnew file mode 100644 index 000000000..f45540b21 --- /dev/null +++ b/res/drawable-mdpi/ic_menu_trash_holo_light.png diff --git a/res/drawable-mdpi/icn_media_pause_focused_holo_dark.png b/res/drawable-mdpi/icn_media_pause_focused_holo_dark.png Binary files differnew file mode 100644 index 000000000..52043b208 --- /dev/null +++ b/res/drawable-mdpi/icn_media_pause_focused_holo_dark.png diff --git a/res/drawable-mdpi/icn_media_pause_normal_holo_dark.png b/res/drawable-mdpi/icn_media_pause_normal_holo_dark.png Binary files differnew file mode 100644 index 000000000..8573b8fd7 --- /dev/null +++ b/res/drawable-mdpi/icn_media_pause_normal_holo_dark.png diff --git a/res/drawable-mdpi/icn_media_pause_pressed_holo_dark.png b/res/drawable-mdpi/icn_media_pause_pressed_holo_dark.png Binary files differnew file mode 100644 index 000000000..afe534b39 --- /dev/null +++ b/res/drawable-mdpi/icn_media_pause_pressed_holo_dark.png diff --git a/res/drawable-mdpi/icn_media_play_focused_holo_dark.png b/res/drawable-mdpi/icn_media_play_focused_holo_dark.png Binary files differnew file mode 100644 index 000000000..c71bcadae --- /dev/null +++ b/res/drawable-mdpi/icn_media_play_focused_holo_dark.png diff --git a/res/drawable-mdpi/icn_media_play_normal_holo_dark.png b/res/drawable-mdpi/icn_media_play_normal_holo_dark.png Binary files differnew file mode 100644 index 000000000..f8d5e690a --- /dev/null +++ b/res/drawable-mdpi/icn_media_play_normal_holo_dark.png diff --git a/res/drawable-mdpi/icn_media_play_pressed_holo_dark.png b/res/drawable-mdpi/icn_media_play_pressed_holo_dark.png Binary files differnew file mode 100644 index 000000000..817f476ab --- /dev/null +++ b/res/drawable-mdpi/icn_media_play_pressed_holo_dark.png diff --git a/res/drawable-mdpi/import_translucent.9.png b/res/drawable-mdpi/import_translucent.9.png Binary files differnew file mode 100644 index 000000000..94a14aebe --- /dev/null +++ b/res/drawable-mdpi/import_translucent.9.png diff --git a/res/drawable-mdpi/manage_bar.9.png b/res/drawable-mdpi/manage_bar.9.png Binary files differnew file mode 100644 index 000000000..e42b92bd3 --- /dev/null +++ b/res/drawable-mdpi/manage_bar.9.png diff --git a/res/drawable-mdpi/manage_frame.9.png b/res/drawable-mdpi/manage_frame.9.png Binary files differnew file mode 100644 index 000000000..879c47ba1 --- /dev/null +++ b/res/drawable-mdpi/manage_frame.9.png diff --git a/res/drawable-mdpi/media_control_bg.9.png b/res/drawable-mdpi/media_control_bg.9.png Binary files differnew file mode 100644 index 000000000..afb7631b1 --- /dev/null +++ b/res/drawable-mdpi/media_control_bg.9.png diff --git a/res/drawable-mdpi/navstrip_translucent.9.png b/res/drawable-mdpi/navstrip_translucent.9.png Binary files differnew file mode 100644 index 000000000..c3a0dc035 --- /dev/null +++ b/res/drawable-mdpi/navstrip_translucent.9.png diff --git a/res/drawable-mdpi/player_scrubber.png b/res/drawable-mdpi/player_scrubber.png Binary files differnew file mode 100644 index 000000000..426e3daff --- /dev/null +++ b/res/drawable-mdpi/player_scrubber.png diff --git a/res/drawable-mdpi/popup_full_dark.9.png b/res/drawable-mdpi/popup_full_dark.9.png Binary files differnew file mode 100644 index 000000000..7b9f2918a --- /dev/null +++ b/res/drawable-mdpi/popup_full_dark.9.png diff --git a/res/drawable-mdpi/preview.png b/res/drawable-mdpi/preview.png Binary files differnew file mode 100644 index 000000000..1f21a5be2 --- /dev/null +++ b/res/drawable-mdpi/preview.png diff --git a/res/drawable-mdpi/progress_bg_holo_dark.9.png b/res/drawable-mdpi/progress_bg_holo_dark.9.png Binary files differnew file mode 100644 index 000000000..c5418f9ef --- /dev/null +++ b/res/drawable-mdpi/progress_bg_holo_dark.9.png diff --git a/res/drawable-mdpi/progress_primary_holo_dark.9.png b/res/drawable-mdpi/progress_primary_holo_dark.9.png Binary files differnew file mode 100644 index 000000000..bac0a23b4 --- /dev/null +++ b/res/drawable-mdpi/progress_primary_holo_dark.9.png diff --git a/res/drawable-mdpi/progress_secondary_holo_dark.9.png b/res/drawable-mdpi/progress_secondary_holo_dark.9.png Binary files differnew file mode 100644 index 000000000..8be8656b6 --- /dev/null +++ b/res/drawable-mdpi/progress_secondary_holo_dark.9.png diff --git a/res/drawable-mdpi/scrollbar_handle_holo_dark.9.png b/res/drawable-mdpi/scrollbar_handle_holo_dark.9.png Binary files differnew file mode 100644 index 000000000..e039c4bca --- /dev/null +++ b/res/drawable-mdpi/scrollbar_handle_holo_dark.9.png diff --git a/res/drawable-mdpi/spinner_76_inner_holo.png b/res/drawable-mdpi/spinner_76_inner_holo.png Binary files differnew file mode 100644 index 000000000..ebccabd98 --- /dev/null +++ b/res/drawable-mdpi/spinner_76_inner_holo.png diff --git a/res/drawable-mdpi/spinner_76_outer_holo.png b/res/drawable-mdpi/spinner_76_outer_holo.png Binary files differnew file mode 100644 index 000000000..37d3f58d0 --- /dev/null +++ b/res/drawable-mdpi/spinner_76_outer_holo.png diff --git a/res/drawable-mdpi/thumbnail_album_video_overlay_holo.png b/res/drawable-mdpi/thumbnail_album_video_overlay_holo.png Binary files differnew file mode 100644 index 000000000..a4541c145 --- /dev/null +++ b/res/drawable-mdpi/thumbnail_album_video_overlay_holo.png diff --git a/res/drawable-mdpi/videooverlay.png b/res/drawable-mdpi/videooverlay.png Binary files differnew file mode 100644 index 000000000..8b39eed6f --- /dev/null +++ b/res/drawable-mdpi/videooverlay.png diff --git a/res/drawable-mdpi/wallpaper_picker_preview.png b/res/drawable-mdpi/wallpaper_picker_preview.png Binary files differnew file mode 100644 index 000000000..452b1251e --- /dev/null +++ b/res/drawable-mdpi/wallpaper_picker_preview.png diff --git a/res/drawable/border_photo_frame_widget.xml b/res/drawable/border_photo_frame_widget.xml new file mode 100644 index 000000000..5d25de533 --- /dev/null +++ b/res/drawable/border_photo_frame_widget.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_focused="true" android:state_pressed="true" android:drawable="@drawable/border_photo_frame_widget_focused_holo" /> + <item android:state_focused="true" android:drawable="@drawable/border_photo_frame_widget_focused_holo" /> + <item android:state_pressed="true" android:drawable="@drawable/border_photo_frame_widget_pressed_holo" /> + <item android:drawable="@drawable/border_photo_frame_widget_holo" /> +</selector> diff --git a/res/drawable/icn_media_pause.xml b/res/drawable/icn_media_pause.xml new file mode 100644 index 000000000..cb5014f50 --- /dev/null +++ b/res/drawable/icn_media_pause.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_pressed="true" + android:drawable="@drawable/icn_media_pause_pressed_holo_dark" /> + <item android:state_focused="true" + android:drawable="@drawable/icn_media_pause_focused_holo_dark" /> + <item android:drawable="@drawable/icn_media_pause_normal_holo_dark" /> +</selector> diff --git a/res/drawable/icn_media_play.xml b/res/drawable/icn_media_play.xml new file mode 100644 index 000000000..a21e0829b --- /dev/null +++ b/res/drawable/icn_media_play.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_pressed="true" + android:drawable="@drawable/icn_media_play_pressed_holo_dark" /> + <item android:state_focused="true" + android:drawable="@drawable/icn_media_play_focused_holo_dark" /> + <item android:drawable="@drawable/icn_media_play_normal_holo_dark" /> +</selector> diff --git a/res/layout/account_header_preference.xml b/res/layout/account_header_preference.xml new file mode 100644 index 000000000..c25058d23 --- /dev/null +++ b/res/layout/account_header_preference.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:gravity="center_vertical" + android:paddingRight="?android:attr/scrollbarSize" + android:paddingLeft="15dp" + android:paddingTop="6dp" + android:paddingBottom="6dp" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <TextView android:id="@+id/title" + android:layout_weight="1" + android:focusable="true" + android:clickable="true" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceMedium" + android:singleLine="true"/> +</LinearLayout> diff --git a/res/layout/account_sync_preference.xml b/res/layout/account_sync_preference.xml new file mode 100644 index 000000000..de41f0b37 --- /dev/null +++ b/res/layout/account_sync_preference.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:gravity="left" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingRight="?android:attr/scrollbarSize" + android:paddingLeft="25dp" + android:paddingTop="6dp" + android:paddingBottom="6dp" + android:layout_weight="1"> + + <TextView android:id="@+id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:focusable="false" + android:clickable="false" + android:singleLine="true" + android:textAppearance="?android:attr/textAppearanceMedium" + android:ellipsize="marquee" + android:fadingEdge="horizontal" /> + + <TextView android:id="@+id/summary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:focusable="false" + android:clickable="false" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="?android:attr/textColorSecondary" + android:maxLines="4"/> +</LinearLayout> diff --git a/res/layout/account_visible_preference.xml b/res/layout/account_visible_preference.xml new file mode 100644 index 000000000..8111e8dfe --- /dev/null +++ b/res/layout/account_visible_preference.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:gravity="center_vertical" + android:paddingRight="?android:attr/scrollbarSize" + android:paddingLeft="25dp" + android:paddingTop="6dp" + android:paddingBottom="6dp" + android:minHeight="?android:attr/listPreferredItemHeight" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <TextView android:id="@+id/title" + android:focusable="false" + android:clickable="false" + android:layout_weight="1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceMedium" + android:maxLines="3"/> + <CheckBox android:id="@+id/checkbox" + android:focusable="false" + android:clickable="false" + android:layout_height="wrap_content" + android:layout_width="wrap_content" /> +</LinearLayout> diff --git a/res/layout/action_mode.xml b/res/layout/action_mode.xml new file mode 100644 index 000000000..d012b7248 --- /dev/null +++ b/res/layout/action_mode.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/navigation_bar" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="horizontal"> + <Button android:id="@+id/selection_menu" + android:divider="?android:attr/listDividerAlertDialog" + style="?android:attr/borderlessButtonStyle" + android:singleLine="true" + android:gravity="left|center_vertical" + android:layout_width="wrap_content" + android:layout_height="match_parent" /> + <ImageView android:layout_marginLeft="8dip" + android:layout_marginRight="8dip" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="bottom" + android:src="@drawable/cab_divider_vertical_dark" /> +</LinearLayout> diff --git a/res/layout/appwidget_loading_item.xml b/res/layout/appwidget_loading_item.xml new file mode 100644 index 000000000..ee8a2063a --- /dev/null +++ b/res/layout/appwidget_loading_item.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/appwidget_photo_border"> + <RelativeLayout + android:layout_width="@dimen/stack_photo_width" + android:layout_height="@dimen/stack_photo_height" + android:background="@android:color/darker_gray"> + <ProgressBar + android:id="@+id/appwidget_loading_item" + android:layout_centerInParent="true" + android:layout_height="wrap_content" + android:layout_width="wrap_content" /> + </RelativeLayout> +</FrameLayout> diff --git a/res/layout/appwidget_main.xml b/res/layout/appwidget_main.xml new file mode 100644 index 000000000..0accabb50 --- /dev/null +++ b/res/layout/appwidget_main.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <RelativeLayout + android:id="@+id/appwidget_empty_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="gone"> + <FrameLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerInParent="true" + android:background="@drawable/appwidget_photo_border"> + <TextView + android:id="@+id/appwidget_photo_item" + android:layout_width="@dimen/stack_photo_width" + android:layout_height="@dimen/stack_photo_height" + android:gravity="center" + android:text="@string/appwidget_empty_text"/> + </FrameLayout> + </RelativeLayout> + <StackView + android:id="@+id/appwidget_stack_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:loopViews="true" /> +</FrameLayout> diff --git a/res/layout/appwidget_photo_item.xml b/res/layout/appwidget_photo_item.xml new file mode 100644 index 000000000..a56a6d7a5 --- /dev/null +++ b/res/layout/appwidget_photo_item.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:background="@drawable/appwidget_photo_border"> + <ImageView + android:id="@+id/appwidget_photo_item" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:scaleType="fitCenter" + android:adjustViewBounds="true" /> +</FrameLayout> diff --git a/res/layout/auto_upload_account_preference.xml b/res/layout/auto_upload_account_preference.xml new file mode 100644 index 000000000..2f7e9e30f --- /dev/null +++ b/res/layout/auto_upload_account_preference.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:gravity="center_vertical" + android:paddingRight="?android:attr/scrollbarSize" + android:paddingLeft="25dp" + android:paddingTop="6dp" + android:paddingBottom="6dp" + android:minHeight="?android:attr/listPreferredItemHeight" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <TextView android:id="@+id/title" + android:layout_weight="1" + android:focusable="false" + android:clickable="false" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceMedium" + android:singleLine="true"/> +</LinearLayout> diff --git a/res/layout/auto_upload_help_text_preference.xml b/res/layout/auto_upload_help_text_preference.xml new file mode 100644 index 000000000..6601ecf2b --- /dev/null +++ b/res/layout/auto_upload_help_text_preference.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:gravity="left" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingRight="?android:attr/scrollbarSize" + android:paddingLeft="15dp" + android:paddingTop="6dp" + android:paddingBottom="6dp" + android:layout_weight="1"> + + <TextView android:id="@+id/summary" + android:focusable="true" + android:clickable="true" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="?android:attr/textColorSecondary" + android:maxLines="4"/> +</LinearLayout> diff --git a/res/layout/cache_notification.xml b/res/layout/cache_notification.xml new file mode 100644 index 000000000..1ffc504db --- /dev/null +++ b/res/layout/cache_notification.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:paddingLeft="16dp" + android:paddingRight="8dp" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <TextView android:layout_width="wrap_content" + android:layout_height="match_parent" + style="@android:style/TextAppearance.StatusBar.EventContent.Title" + android:text="@string/cache_status_title"/> + <TextView android:id="@+id/status" + style="@android:style/TextAppearance.StatusBar.EventContent" + android:layout_width="wrap_content" + android:layout_height="match_parent"/> + <ProgressBar android:id="@+id/progress" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> +</LinearLayout> diff --git a/res/layout/choose_widget_type.xml b/res/layout/choose_widget_type.xml new file mode 100644 index 000000000..7da8dd157 --- /dev/null +++ b/res/layout/choose_widget_type.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<RadioGroup xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/widget_type" + android:paddingLeft="32dp" + android:paddingRight="32dp" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + <RadioButton android:id="@+id/widget_type_album" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:minHeight="48dp" + android:text="@string/widget_type_album"/> + <RadioButton android:id="@+id/widget_type_shuffle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:minHeight="48dp" + android:text="@string/widget_type_shuffle"/> + <RadioButton android:id="@+id/widget_type_photo" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:minHeight="48dp" + android:text="@string/widget_type_photo"/> + <View android:layout_width="match_parent" + android:layout_height="1dp" + android:layout_weight="0" + android:layout_marginLeft="16dp" + android:layout_marginRight="16dp" + android:background="?android:attr/dividerHorizontal" /> + <Button style="?android:attr/buttonBarButtonStyle" + android:id="@+id/cancel" + android:layout_weight="0" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@android:string/cancel" /> +</RadioGroup> diff --git a/res/layout/cropimage.xml b/res/layout/cropimage.xml new file mode 100644 index 000000000..aefebe82d --- /dev/null +++ b/res/layout/cropimage.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2007 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. +--> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <view class="com.android.gallery3d.ui.GLRootView" + android:id="@+id/gl_root_view" + android:background="@null" + android:layout_width="match_parent" + android:layout_height="match_parent" /> +</FrameLayout> diff --git a/res/layout/dialog_picker.xml b/res/layout/dialog_picker.xml new file mode 100644 index 000000000..6dfa9d90d --- /dev/null +++ b/res/layout/dialog_picker.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <com.android.gallery3d.ui.GLRootView + android:id="@+id/gl_root_view" + android:layout_weight="1" + android:layout_height="0dp" + android:layout_width="match_parent"/> + <View android:layout_width="match_parent" + android:layout_height="1dp" + android:layout_weight="0" + android:layout_marginLeft="16dp" + android:layout_marginRight="16dp" + android:background="?android:attr/dividerHorizontal" /> + <Button style="?android:attr/buttonBarButtonStyle" + android:id="@+id/cancel" + android:layout_weight="0" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@android:string/cancel" /> +</LinearLayout> diff --git a/res/layout/main.xml b/res/layout/main.xml new file mode 100644 index 000000000..d5188330e --- /dev/null +++ b/res/layout/main.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <com.android.gallery3d.ui.GLRootView + android:id="@+id/gl_root_view" + android:layout_width="match_parent" + android:layout_height="match_parent"/> +</LinearLayout> diff --git a/res/layout/movie_view.xml b/res/layout/movie_view.xml new file mode 100644 index 000000000..6d6b28d1c --- /dev/null +++ b/res/layout/movie_view.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2007 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. +--> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/root" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <VideoView android:id="@+id/surface_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_centerInParent="true" /> + + <LinearLayout android:id="@+id/progress_indicator" + android:orientation="vertical" + android:layout_centerInParent="true" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <ProgressBar android:id="@android:id/progress" + style="?android:attr/progressBarStyleLarge" + android:layout_gravity="center" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + <TextView android:paddingTop="5dip" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:text="@string/loading_video" android:textSize="14sp" + android:textColor="#ffffffff" /> + </LinearLayout> + +</RelativeLayout> diff --git a/res/layout/photo_frame.xml b/res/layout/photo_frame.xml new file mode 100755 index 000000000..deadaebc1 --- /dev/null +++ b/res/layout/photo_frame.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2009 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. +--> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingTop="4dp" + android:paddingBottom="23dp" + android:paddingLeft="12dp" + android:paddingRight="12dp"> + <ImageView android:id="@+id/photo" + android:layout_gravity="center" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:adjustViewBounds="true" + android:scaleType="fitCenter" + android:cropToPadding="true" + android:background="@drawable/border_photo_frame_widget"/> +</FrameLayout> diff --git a/res/menu/album.xml b/res/menu/album.xml new file mode 100644 index 000000000..1e1f6ef1e --- /dev/null +++ b/res/menu/album.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 Google Inc. + + 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. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/action_slideshow" + android:icon="@drawable/ic_menu_slideshow_holo_light" + android:title="@string/slideshow" + android:showAsAction="ifRoom" /> + <item android:id="@+id/action_select" + android:title="@string/select_item" + android:showAsAction="never" /> + <item android:id="@+id/action_group_by" + android:title="@string/group_by" + android:showAsAction="never"/> +</menu> diff --git a/res/menu/albumset.xml b/res/menu/albumset.xml new file mode 100644 index 000000000..3bb46f7d1 --- /dev/null +++ b/res/menu/albumset.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 Google Inc. + + 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. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/action_camera" + android:icon="@drawable/ic_menu_camera_holo_light" + android:title="@string/switch_to_camera" + android:showAsAction="ifRoom" /> + <item android:id="@+id/action_select" + android:title="@string/select_album" + android:showAsAction="never" /> + <item android:id="@+id/action_manage_offline" + android:title="@string/make_available_offline" + android:showAsAction="never" /> + <item android:id="@+id/action_sync_picasa_albums" + android:title="@string/sync_picasa_albums" + android:showAsAction="never" /> + <item android:id="@+id/action_settings" + android:title="@string/settings" + android:showAsAction="never" /> +</menu> diff --git a/res/menu/crop.xml b/res/menu/crop.xml new file mode 100644 index 000000000..addd26ff4 --- /dev/null +++ b/res/menu/crop.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 Google Inc. + + 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. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/save" + android:icon="@drawable/ic_menu_save_holo_light" + android:title="@string/crop_save_text" + android:showAsAction="always|withText"> + </item> + <item android:id="@+id/cancel" + android:icon="@drawable/ic_menu_cancel_holo_light" + android:title="@android:string/cancel" + android:showAsAction="always"> + </item> +</menu> diff --git a/res/menu/filterby.xml b/res/menu/filterby.xml new file mode 100644 index 000000000..3a72c57b2 --- /dev/null +++ b/res/menu/filterby.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 Google Inc. + + 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. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/action_filter_all" + android:title="@string/show_all" /> + <item android:id="@+id/action_filter_image" + android:title="@string/show_images_only" /> + <item android:id="@+id/action_filter_video" + android:title="@string/show_videos_only" /> +</menu> diff --git a/res/menu/groupby.xml b/res/menu/groupby.xml new file mode 100644 index 000000000..b2c2b8d59 --- /dev/null +++ b/res/menu/groupby.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 Google Inc. + + 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. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/action_cluster_album" + android:title="@string/group_by_album" /> + <item android:id="@+id/action_cluster_time" + android:title="@string/group_by_time" /> + <item android:id="@+id/action_cluster_location" + android:title="@string/group_by_location" /> + <item android:id="@+id/action_cluster_tags" + android:title="@string/group_by_tags" /> + <item android:id="@+id/action_cluster_size" + android:title="@string/group_by_size" /> + <item android:id="@+id/action_cluster_faces" + android:title="@string/group_by_faces" /> +</menu> diff --git a/res/menu/operation.xml b/res/menu/operation.xml new file mode 100644 index 000000000..334b33400 --- /dev/null +++ b/res/menu/operation.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 Google Inc. + + 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. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/action_import" + android:title="@string/Import" + android:icon="@drawable/ic_menu_ptp_holo_light" + android:showAsAction="always|withText" + android:visible="false" /> + <item android:id="@+id/action_share" + android:icon="@drawable/ic_menu_share_holo_light" + android:title="@string/share" + android:actionProviderClass="android.widget.ShareActionProvider" + android:showAsAction="ifRoom"> + </item> + <item android:id="@+id/action_delete" + android:icon="@drawable/ic_menu_trash_holo_light" + android:title="@string/delete" + android:showAsAction="ifRoom"> + <menu> + <item android:id="@+id/action_confirm_delete" + android:icon="@drawable/ic_menu_trash_holo_light" + android:title="@string/confirm_delete" /> + <item android:id="@+id/action_cancel_delete" + android:icon="@drawable/ic_menu_cancel_holo_light" + android:title="@string/cancel" /> + </menu> + </item> + <item android:id="@+id/action_show_on_map" + android:title="@string/show_on_map" + android:showAsAction="never" + android:visible="false" /> + <item android:id="@+id/action_rotate_ccw" + android:showAsAction="never" + android:title="@string/rotate_left" /> + <item android:id="@+id/action_rotate_cw" + android:showAsAction="never" + android:title="@string/rotate_right" /> + <item android:id="@+id/action_setas" + android:title="@string/set_image" + android:showAsAction="never" + android:visible="false" /> + <item android:id="@+id/action_crop" + android:title="@string/crop" + android:showAsAction="never" + android:visible="false" /> + <item android:id="@+id/action_details" + android:icon="@drawable/ic_menu_info_details" + android:title="@string/details" + android:showAsAction="never" /> + <item android:id="@+id/action_edit" + android:title="@string/edit" + android:showAsAction="never" + android:visible="false" /> +</menu> diff --git a/res/menu/photo.xml b/res/menu/photo.xml new file mode 100644 index 000000000..d01ba2859 --- /dev/null +++ b/res/menu/photo.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 Google Inc. + + 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. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/action_share" + android:icon="@drawable/ic_menu_share_holo_light" + android:title="@string/share" + android:enabled="true" + android:actionProviderClass="android.widget.ShareActionProvider" + android:showAsAction="always"> + <!-- We need this to create a dynamic list of submenu --> + <menu /> + </item> + <item android:id="@+id/action_delete" + android:icon="@drawable/ic_menu_trash_holo_light" + android:title="@string/delete" + android:showAsAction="always"> + <menu> + <item android:id="@+id/action_confirm_delete" + android:icon="@drawable/ic_menu_trash_holo_light" + android:title="@string/confirm_delete" /> + <item android:id="@+id/action_cancel_delete" + android:icon="@drawable/ic_menu_cancel_holo_light" + android:title="@string/cancel" /> + </menu> + </item> + <item android:id="@+id/action_slideshow" + android:icon="@drawable/ic_menu_slideshow_holo_light" + android:title="@string/slideshow" + android:showAsAction="always" /> + <item android:id="@+id/action_import" + android:title="@string/Import" + android:icon="@drawable/ic_menu_ptp_holo_light" + android:showAsAction="always|withText" + android:visible="false" /> + <item android:id="@+id/action_details" + android:title="@string/details" + android:showAsAction="never" /> + <item android:id="@+id/action_show_on_map" + android:title="@string/show_on_map" + android:showAsAction="never" /> + <item android:id="@+id/action_rotate_ccw" + android:showAsAction="never" + android:title="@string/rotate_left" /> + <item android:id="@+id/action_rotate_cw" + android:showAsAction="never" + android:title="@string/rotate_right" /> + <item android:id="@+id/action_setas" + android:title="@string/set_image" + android:showAsAction="never" /> + <item android:id="@+id/action_crop" + android:title="@string/crop" + android:showAsAction="never" /> + <item android:id="@+id/action_edit" + android:title="@string/edit" + android:showAsAction="never" + android:visible="false" /> +</menu> diff --git a/res/menu/pickup.xml b/res/menu/pickup.xml new file mode 100644 index 000000000..f22bc6dbe --- /dev/null +++ b/res/menu/pickup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 Google Inc. + + 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. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/action_cancel" + android:icon="@drawable/ic_menu_cancel_holo_light" + android:title="@string/cancel" + android:showAsAction="always|withText" /> +</menu> diff --git a/res/menu/selection.xml b/res/menu/selection.xml new file mode 100644 index 000000000..18839e4d2 --- /dev/null +++ b/res/menu/selection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 Google Inc. + + 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. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/action_select_all" android:title="@string/select_all" /> +</menu> diff --git a/res/menu/settings.xml b/res/menu/settings.xml new file mode 100644 index 000000000..f91f1bac7 --- /dev/null +++ b/res/menu/settings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/add_account" + android:title="@string/add_account" + android:showAsAction="always|withText"> + </item> +</menu> diff --git a/res/mipmap-hdpi/ic_launcher_gallery.png b/res/mipmap-hdpi/ic_launcher_gallery.png Binary files differnew file mode 100644 index 000000000..34410f80c --- /dev/null +++ b/res/mipmap-hdpi/ic_launcher_gallery.png diff --git a/res/mipmap-mdpi/ic_launcher_gallery.png b/res/mipmap-mdpi/ic_launcher_gallery.png Binary files differnew file mode 100644 index 000000000..3a701bc94 --- /dev/null +++ b/res/mipmap-mdpi/ic_launcher_gallery.png diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml new file mode 100644 index 000000000..18aab28a3 --- /dev/null +++ b/res/values-af/strings.xml @@ -0,0 +1,231 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galery"</string> + <string name="gadget_title" msgid="259405922673466798">"Prentraam"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <!-- outdated translation 3697303290960009886 --> <string name="movie_view_label" msgid="3526526872644898229">"Flieks"</string> + <string name="loading_video" msgid="4013492720121891585">"Laai tans video…"</string> + <string name="loading_image" msgid="1200894415793838191">"Laai tans beeld…"</string> + <!-- no translation found for loading_account (928195413034552034) --> + <skip /> + <string name="resume_playing_title" msgid="8996677350649355013">"Hervat video"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Hervat speel vanaf %s ?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Speel verder"</string> + <string name="loading" msgid="7038208555304563571">"Laai tans…"</string> + <!-- outdated translation 3355969119388837437 --> <string name="fail_to_load" msgid="2710120770735315683">"Kon nie laai nie"</string> + <!-- no translation found for no_thumbnail (284723185546429750) --> + <skip /> + <string name="resume_playing_restart" msgid="5471008499835769292">"Begin van voor af"</string> + <string name="crop_save_text" msgid="8821167985419282305">"Ok"</string> + <!-- no translation found for multiface_crop_help (3127018992717032779) --> + <skip /> + <string name="saving_image" msgid="7270334453636349407">"Stoor tans prent…"</string> + <!-- no translation found for crop_label (521114301871349328) --> + <skip /> + <string name="select_image" msgid="7841406150484742140">"Kies foto"</string> + <string name="select_video" msgid="4859510992798615076">"Kies video"</string> + <string name="select_item" msgid="2257529413100472599">"Kies item(s)"</string> + <string name="select_album" msgid="4632641262236697235">"Kies album(s)"</string> + <string name="select_group" msgid="9090385962030340391">"Kies groep(e)"</string> + <string name="set_image" msgid="2331476809308010401">"Stel prent as"</string> + <!-- no translation found for wallpaper (9222901738515471972) --> + <skip /> + <!-- no translation found for camera_setas_wallpaper (797463183863414289) --> + <skip /> + <!-- no translation found for delete (2839695998251824487) --> + <skip /> + <string name="confirm_delete" msgid="5731757674837098707">"Bevestig uitvee"</string> + <!-- no translation found for cancel (3637516880917356226) --> + <skip /> + <string name="share" msgid="3619042788254195341">"Deling"</string> + <string name="select_all" msgid="8623593677101437957">"Kies almal"</string> + <string name="deselect_all" msgid="7397531298370285581">"Ontmerk almal"</string> + <string name="slideshow" msgid="4355906903247112975">"Skyfievertoning"</string> + <!-- no translation found for details (8415120088556445230) --> + <skip /> + <!-- no translation found for switch_to_camera (7280111806675169992) --> + <skip /> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"%1$d gekies"</item> + <item quantity="one" msgid="2478365152745637768">"%1$d gekies"</item> + <item quantity="other" msgid="754722656147810487">"%1$d gekies"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"%1$d gekies"</item> + <item quantity="one" msgid="6184377003099987825">"%1$d gekies"</item> + <item quantity="other" msgid="53105607141906130">"%1$d gekies"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"%1$d gekies"</item> + <item quantity="one" msgid="5030162638216034260">"%1$d gekies"</item> + <item quantity="other" msgid="3512041363942842738">"%1$d gekies"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"Wys op kaart"</string> + <string name="rotate_left" msgid="7412075232752726934">"Draai na links"</string> + <string name="rotate_right" msgid="7340681085011826618">"Draai na regs"</string> + <string name="no_such_item" msgid="3161074758669642065">"Item nie gevind nie"</string> + <!-- no translation found for edit (1502273844748580847) --> + <skip /> + <string name="activity_not_found" msgid="3731390759313019518">"Geen program beskikbaar nie"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Verwerk kasversoeke"</string> + <string name="caching_label" msgid="3244800874547101776">"Kas tans..."</string> + <string name="crop" msgid="7970750655414797277">"Snoei"</string> + <string name="set_as" msgid="3636764710790507868">"Stel as"</string> + <string name="video_err" msgid="7917736494827857757">"Kan nie video speel nie"</string> + <string name="group_by_location" msgid="316641628989023253">"Volgens ligging"</string> + <string name="group_by_time" msgid="9046168567717963573">"Volgens tyd"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Volgens merkers"</string> + <!-- no translation found for group_by_faces (1566351636227274906) --> + <skip /> + <string name="group_by_album" msgid="1532818636053818958">"Volgens album"</string> + <!-- no translation found for group_by_size (153766174950394155) --> + <skip /> + <string name="untagged" msgid="7281481064509590402">"Ongemerk"</string> + <string name="no_location" msgid="2036710947563713111">"Geen ligging nie"</string> + <string name="show_images_only" msgid="7263218480867672653">"Net prente"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Net video\'s"</string> + <string name="show_all" msgid="4780647751652596980">"Prente en video\'s"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Fotogalery"</string> + <!-- outdated translation 9162928643614581527 --> <string name="appwidget_empty_text" msgid="4123016777080388680">"Geen foto\'s in galery nie"</string> + <string name="crop_saved" msgid="4684933379430649946">"Die gesnoeide prent is gestoor in aflaaisels"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Die gesnoeide prent is nie gestoor nie"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Daar is geen albums beskikbaar nie"</string> + <string name="empty_album" msgid="6307897398825514762">"Daar is geen prente/video\'s beskikbaar nie"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa Web Albums"</string> + <!-- no translation found for picasa_posts (1055151689217481993) --> + <skip /> + <string name="make_available_offline" msgid="5157950985488297112">"Maak vanlyn beskikbaar"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Klaar"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d van %2$d items:"</string> + <string name="title" msgid="7622928349908052569">"Titel"</string> + <string name="description" msgid="3016729318096557520">"Beskrywing"</string> + <string name="time" msgid="1367953006052876956">"Tyd"</string> + <string name="location" msgid="3432705876921618314">"Ligging"</string> + <string name="path" msgid="4725740395885105824">"Pad"</string> + <string name="width" msgid="9215847239714321097">"Wydte"</string> + <string name="height" msgid="3648885449443787772">"Hoogte"</string> + <string name="orientation" msgid="4958327983165245513">"Oriëntasie"</string> + <string name="duration" msgid="8160058911218541616">"Tydsduur"</string> + <string name="mimetype" msgid="3518268469266183548">"MIME-tipe"</string> + <string name="file_size" msgid="4670384449129762138">"Lêergrootte"</string> + <string name="maker" msgid="7921835498034236197">"Maker"</string> + <string name="model" msgid="8240207064064337366">"Model"</string> + <string name="flash" msgid="2816779031261147723">"Flits"</string> + <string name="aperture" msgid="5920657630303915195">"Apertuur"</string> + <string name="focal_length" msgid="1291383769749877010">"Fokuslengte"</string> + <string name="white_balance" msgid="8122534414851280901">"Witbalans"</string> + <string name="exposure_time" msgid="3146642210127439553">"Beligtingstyd"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Handmatig"</string> + <string name="auto" msgid="4296941368722892821">"Outo"</string> + <string name="flash_on" msgid="7891556231891837284">"Flits gevuur"</string> + <string name="flash_off" msgid="1445443413822680010">"Geen flits"</string> + <!-- no translation found for make_albums_available_offline:one (2955975726887896888) --> + <!-- no translation found for make_albums_available_offline:other (6929905722448632886) --> + <!-- no translation found for try_to_set_local_album_available_offline (2184754031896160755) --> + <skip /> + <!-- no translation found for set_label_all_albums (3507256844918130594) --> + <skip /> + <!-- no translation found for set_label_local_albums (5227548825039781) --> + <skip /> + <!-- no translation found for set_label_mtp_devices (5779788799122828528) --> + <skip /> + <!-- no translation found for set_label_picasa_albums (2736308697306982589) --> + <skip /> + <!-- no translation found for free_space_format (8766337315709161215) --> + <skip /> + <!-- no translation found for size_below (2074956730721942260) --> + <skip /> + <!-- no translation found for size_above (5324398253474104087) --> + <skip /> + <!-- no translation found for size_between (8779660840898917208) --> + <skip /> + <!-- no translation found for Import (3985447518557474672) --> + <skip /> + <!-- no translation found for import_complete (1098450310074640619) --> + <skip /> + <!-- no translation found for import_fail (5205927625132482529) --> + <skip /> + <!-- no translation found for camera_connected (6984353643349303075) --> + <skip /> + <!-- no translation found for camera_disconnected (3683036560562699311) --> + <skip /> + <!-- no translation found for click_import (6407959065464291972) --> + <skip /> + <!-- no translation found for widget_type_album (3245149644830731121) --> + <skip /> + <!-- no translation found for widget_type_shuffle (8594622705019763768) --> + <skip /> + <!-- no translation found for widget_type_photo (8384174698965738770) --> + <skip /> + <!-- no translation found for widget_type (7308564524449340985) --> + <skip /> + <!-- no translation found for slideshow_dream_name (6915963319933437083) --> + <skip /> + <!-- no translation found for cache_status_title (8414708919928621485) --> + <skip /> + <!-- no translation found for cache_status (7690438435538533106) --> + <skip /> + <!-- no translation found for cache_done (9194449192869777483) --> + <skip /> + <!-- no translation found for albums (7320787705180057947) --> + <skip /> + <string name="times" msgid="2023033894889499219">"Tye"</string> + <!-- no translation found for locations (6649297994083130305) --> + <skip /> + <!-- no translation found for people (4114003823747292747) --> + <skip /> + <!-- no translation found for tags (5539648765482935955) --> + <skip /> + <string name="group_by" msgid="4308299657902209357">"Groepeer volgens"</string> + <!-- no translation found for settings (1534847740615665736) --> + <skip /> + <!-- no translation found for prefs_accounts (7942761992713671670) --> + <skip /> + <!-- no translation found for prefs_data_usage (410592732727343215) --> + <skip /> + <!-- no translation found for prefs_auto_upload (2467627128066665126) --> + <skip /> + <!-- no translation found for prefs_other_settings (6034181851440646681) --> + <skip /> + <!-- no translation found for about_gallery (8667445445883757255) --> + <skip /> + <!-- no translation found for sync_on_wifi_only (5795753226259399958) --> + <skip /> + <!-- no translation found for helptext_auto_upload (133741242503097377) --> + <skip /> + <!-- no translation found for enable_auto_upload (1586329406342131) --> + <skip /> + <!-- no translation found for photo_sync_is_on (1653898269297050634) --> + <skip /> + <!-- no translation found for photo_sync_is_off (6464193461664544289) --> + <skip /> + <!-- no translation found for helptext_photo_sync (8617245939103545623) --> + <skip /> + <!-- no translation found for view_photo_for_account (5608040380422337939) --> + <skip /> + <!-- no translation found for add_account (4271217504968243974) --> + <skip /> + <!-- no translation found for auto_upload_chooser_title (1494524693870792948) --> + <skip /> +</resources> diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml new file mode 100644 index 000000000..1dd899c2b --- /dev/null +++ b/res/values-am/strings.xml @@ -0,0 +1,233 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"የሥነ ጥበብ ማዕከል"</string> + <string name="gadget_title" msgid="259405922673466798">"የምስል ክፈፍ"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <!-- outdated translation 3697303290960009886 --> <string name="movie_view_label" msgid="3526526872644898229">"ፊልሞች"</string> + <string name="loading_video" msgid="4013492720121891585">"ቪዲዮ በማስገባት ላይ"</string> + <string name="loading_image" msgid="1200894415793838191">"ምስል በመስቀል ላይ...."</string> + <!-- no translation found for loading_account (928195413034552034) --> + <skip /> + <string name="resume_playing_title" msgid="8996677350649355013">"ቪዲዮ ቀጥል"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"ከ%s ማጫወት ይቀጥል?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"ማጫወት ቀጥል"</string> + <string name="loading" msgid="7038208555304563571">"በመስቀል ላይ…"</string> + <!-- outdated translation 3355969119388837437 --> <string name="fail_to_load" msgid="2710120770735315683">"ለመስቀል ተስኗል"</string> + <!-- no translation found for no_thumbnail (284723185546429750) --> + <skip /> + <string name="resume_playing_restart" msgid="5471008499835769292">"እንደገና ጀምር"</string> + <string name="crop_save_text" msgid="8821167985419282305">"እሺ"</string> + <!-- no translation found for multiface_crop_help (3127018992717032779) --> + <skip /> + <string name="saving_image" msgid="7270334453636349407">"ምስል በማስቀመጥ ላይ..."</string> + <!-- no translation found for crop_label (521114301871349328) --> + <skip /> + <string name="select_image" msgid="7841406150484742140">"ፎቶዎች ምረጥ"</string> + <string name="select_video" msgid="4859510992798615076">"ቪዲዮ ምረጥ"</string> + <string name="select_item" msgid="2257529413100472599">"ዓይነት(ኦች) ምረጥ"</string> + <string name="select_album" msgid="4632641262236697235">"አልበም(ኦች) ምረጥ"</string> + <string name="select_group" msgid="9090385962030340391">"ቡድን(ኦች) ምረጥ"</string> + <string name="set_image" msgid="2331476809308010401">"ምስል እንደ አዘጋጅ"</string> + <!-- no translation found for wallpaper (9222901738515471972) --> + <skip /> + <!-- no translation found for camera_setas_wallpaper (797463183863414289) --> + <skip /> + <!-- no translation found for delete (2839695998251824487) --> + <skip /> + <string name="confirm_delete" msgid="5731757674837098707">"ስረዛ አረጋግጥ"</string> + <!-- no translation found for cancel (3637516880917356226) --> + <skip /> + <string name="share" msgid="3619042788254195341">"አጋራ"</string> + <string name="select_all" msgid="8623593677101437957">"ሁሉንም ምረጥ"</string> + <string name="deselect_all" msgid="7397531298370285581">"ሁሉንም አትምረጥ"</string> + <string name="slideshow" msgid="4355906903247112975">"ስላይድ አሳይ"</string> + <!-- no translation found for details (8415120088556445230) --> + <skip /> + <!-- no translation found for switch_to_camera (7280111806675169992) --> + <skip /> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"%1$d ተመርጠዋል"</item> + <item quantity="one" msgid="2478365152745637768">"%1$d ተመርጠዋል"</item> + <item quantity="other" msgid="754722656147810487">"%1$d ተመርጠዋል"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"%1$d ተመርጠዋል"</item> + <item quantity="one" msgid="6184377003099987825">"%1$d ተመርጠዋል"</item> + <item quantity="other" msgid="53105607141906130">"%1$d ተመርጠዋል"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"%1$d ተመርጠዋል"</item> + <item quantity="one" msgid="5030162638216034260">"%1$d ተመርጠዋል"</item> + <item quantity="other" msgid="3512041363942842738">"%1$d ተመርጠዋል"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"ካርታ ላይ አሳይ"</string> + <string name="rotate_left" msgid="7412075232752726934">"ወደ ግራ አሽከርክር"</string> + <string name="rotate_right" msgid="7340681085011826618">"ወደ ቀኝ አሽከርክር"</string> + <string name="no_such_item" msgid="3161074758669642065">"አይነት አልተገኘም"</string> + <!-- no translation found for edit (1502273844748580847) --> + <skip /> + <string name="activity_not_found" msgid="3731390759313019518">"ምንም ትግበራ የለም"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"ሂደት መሸጎጫ ጥየቃዎች"</string> + <string name="caching_label" msgid="3244800874547101776">"በመሸጎጥ ላይ..."</string> + <string name="crop" msgid="7970750655414797277">"ክፈፍ"</string> + <string name="set_as" msgid="3636764710790507868">"እንደ"</string> + <string name="video_err" msgid="7917736494827857757">"ቪዲዮ ለማጫወት አልተቻለም"</string> + <string name="group_by_location" msgid="316641628989023253">"በስፍራ"</string> + <string name="group_by_time" msgid="9046168567717963573">"በጊዜ"</string> + <string name="group_by_tags" msgid="3568731317210676160">"በመለያዎች"</string> + <!-- no translation found for group_by_faces (1566351636227274906) --> + <skip /> + <string name="group_by_album" msgid="1532818636053818958">"በ አልበም"</string> + <!-- no translation found for group_by_size (153766174950394155) --> + <skip /> + <string name="untagged" msgid="7281481064509590402">"ያልተለጠፈ"</string> + <string name="no_location" msgid="2036710947563713111">"ምንም ስፍራ"</string> + <string name="show_images_only" msgid="7263218480867672653">"ምስሎች ብቻ"</string> + <string name="show_videos_only" msgid="3850394623678871697">"ቪዲዮዎች ብቻ"</string> + <string name="show_all" msgid="4780647751652596980">"ምስሎች እና ቪዲዮዎች"</string> + <!-- no translation found for appwidget_title (6410561146863700411) --> + <skip /> + <!-- no translation found for appwidget_empty_text (4123016777080388680) --> + <skip /> + <string name="crop_saved" msgid="4684933379430649946">"የተከረከመው ምስል በአውርድ ውስጥ ተቀምጧል"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"የተከረከመው ምስል አልተቀመጠም"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"ምንም አልበሞች የሉም።"</string> + <string name="empty_album" msgid="6307897398825514762">"ምንም ምስሎች/ቪዲዮዎች የሉም"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"ፒካሳ ድረ አልበሞች"</string> + <!-- no translation found for picasa_posts (1055151689217481993) --> + <skip /> + <string name="make_available_offline" msgid="5157950985488297112">"ከመስመር ውጪ እንዲገኝአድርግ"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"ተከናውኗል"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d ከ%2$d አይነቶች:"</string> + <string name="title" msgid="7622928349908052569">"አርዕስት"</string> + <string name="description" msgid="3016729318096557520">"መግለጫ"</string> + <string name="time" msgid="1367953006052876956">"ጊዜ"</string> + <string name="location" msgid="3432705876921618314">"ስፍራ"</string> + <string name="path" msgid="4725740395885105824">"ዱካ"</string> + <string name="width" msgid="9215847239714321097">"ስፋት"</string> + <string name="height" msgid="3648885449443787772">"ቁመት"</string> + <string name="orientation" msgid="4958327983165245513">"አቀማመጠ ገፅ"</string> + <string name="duration" msgid="8160058911218541616">"የጊዜ መጠን፡"</string> + <string name="mimetype" msgid="3518268469266183548">"MIME አይነት"</string> + <string name="file_size" msgid="4670384449129762138">"የፋይል መጠን"</string> + <string name="maker" msgid="7921835498034236197">"ሰሪ"</string> + <string name="model" msgid="8240207064064337366">"ሞዴል"</string> + <string name="flash" msgid="2816779031261147723">"ፍላሽ"</string> + <string name="aperture" msgid="5920657630303915195">"የካሜራ ሌንስ ማስገቢያ"</string> + <string name="focal_length" msgid="1291383769749877010">"የትኩረት ርዝመት"</string> + <string name="white_balance" msgid="8122534414851280901">"ዝግጁ ምስል"</string> + <string name="exposure_time" msgid="3146642210127439553">"የብርሃነ መጠን ጊዜ"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"መመሪያ"</string> + <string name="auto" msgid="4296941368722892821">"ራስ ሰር"</string> + <string name="flash_on" msgid="7891556231891837284">"ብልጭ ብሏል"</string> + <string name="flash_off" msgid="1445443413822680010">"ምንም ብልጭታ"</string> + <!-- no translation found for make_albums_available_offline:one (2955975726887896888) --> + <!-- no translation found for make_albums_available_offline:other (6929905722448632886) --> + <!-- no translation found for try_to_set_local_album_available_offline (2184754031896160755) --> + <skip /> + <!-- no translation found for set_label_all_albums (3507256844918130594) --> + <skip /> + <!-- no translation found for set_label_local_albums (5227548825039781) --> + <skip /> + <!-- no translation found for set_label_mtp_devices (5779788799122828528) --> + <skip /> + <!-- no translation found for set_label_picasa_albums (2736308697306982589) --> + <skip /> + <!-- no translation found for free_space_format (8766337315709161215) --> + <skip /> + <!-- no translation found for size_below (2074956730721942260) --> + <skip /> + <!-- no translation found for size_above (5324398253474104087) --> + <skip /> + <!-- no translation found for size_between (8779660840898917208) --> + <skip /> + <!-- no translation found for Import (3985447518557474672) --> + <skip /> + <!-- no translation found for import_complete (1098450310074640619) --> + <skip /> + <!-- no translation found for import_fail (5205927625132482529) --> + <skip /> + <!-- no translation found for camera_connected (6984353643349303075) --> + <skip /> + <!-- no translation found for camera_disconnected (3683036560562699311) --> + <skip /> + <!-- no translation found for click_import (6407959065464291972) --> + <skip /> + <!-- no translation found for widget_type_album (3245149644830731121) --> + <skip /> + <!-- no translation found for widget_type_shuffle (8594622705019763768) --> + <skip /> + <!-- no translation found for widget_type_photo (8384174698965738770) --> + <skip /> + <!-- no translation found for widget_type (7308564524449340985) --> + <skip /> + <!-- no translation found for slideshow_dream_name (6915963319933437083) --> + <skip /> + <!-- no translation found for cache_status_title (8414708919928621485) --> + <skip /> + <!-- no translation found for cache_status (7690438435538533106) --> + <skip /> + <!-- no translation found for cache_done (9194449192869777483) --> + <skip /> + <!-- no translation found for albums (7320787705180057947) --> + <skip /> + <string name="times" msgid="2023033894889499219">"ጊዜ"</string> + <!-- no translation found for locations (6649297994083130305) --> + <skip /> + <!-- no translation found for people (4114003823747292747) --> + <skip /> + <!-- no translation found for tags (5539648765482935955) --> + <skip /> + <string name="group_by" msgid="4308299657902209357">"በቡድን አስቀምጥ"</string> + <!-- no translation found for settings (1534847740615665736) --> + <skip /> + <!-- no translation found for prefs_accounts (7942761992713671670) --> + <skip /> + <!-- no translation found for prefs_data_usage (410592732727343215) --> + <skip /> + <!-- no translation found for prefs_auto_upload (2467627128066665126) --> + <skip /> + <!-- no translation found for prefs_other_settings (6034181851440646681) --> + <skip /> + <!-- no translation found for about_gallery (8667445445883757255) --> + <skip /> + <!-- no translation found for sync_on_wifi_only (5795753226259399958) --> + <skip /> + <!-- no translation found for helptext_auto_upload (133741242503097377) --> + <skip /> + <!-- no translation found for enable_auto_upload (1586329406342131) --> + <skip /> + <!-- no translation found for photo_sync_is_on (1653898269297050634) --> + <skip /> + <!-- no translation found for photo_sync_is_off (6464193461664544289) --> + <skip /> + <!-- no translation found for helptext_photo_sync (8617245939103545623) --> + <skip /> + <!-- no translation found for view_photo_for_account (5608040380422337939) --> + <skip /> + <!-- no translation found for add_account (4271217504968243974) --> + <skip /> + <!-- no translation found for auto_upload_chooser_title (1494524693870792948) --> + <skip /> +</resources> diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml new file mode 100644 index 000000000..38db52272 --- /dev/null +++ b/res/values-ar/strings.xml @@ -0,0 +1,172 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"المعرض"</string> + <string name="gadget_title" msgid="259405922673466798">"إطار الصورة"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"مشغّل الفيديو"</string> + <string name="loading_video" msgid="4013492720121891585">"جارٍ تحميل الفيديو…"</string> + <string name="loading_image" msgid="1200894415793838191">"جارٍ تحميل الصورة…"</string> + <string name="loading_account" msgid="928195413034552034">"جارٍ تحميل الحساب..."</string> + <string name="resume_playing_title" msgid="8996677350649355013">"استئناف الفيديو"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"هل تريد استئناف التشغيل من %s ؟"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"استئناف التشغيل"</string> + <string name="loading" msgid="7038208555304563571">"جارٍ التحميل…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"أخفق التحميل."</string> + <string name="no_thumbnail" msgid="284723185546429750">"بلا صورة مصغرة"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"البدء من جديد"</string> + <string name="crop_save_text" msgid="8821167985419282305">"موافق"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"انقر على وجه للبدء."</string> + <string name="saving_image" msgid="7270334453636349407">"جارٍ حفظ الصورة..."</string> + <string name="crop_label" msgid="521114301871349328">"اقتصاص الصورة"</string> + <string name="select_image" msgid="7841406150484742140">"تحديد صورة"</string> + <string name="select_video" msgid="4859510992798615076">"تحديد فيديو"</string> + <string name="select_item" msgid="2257529413100472599">"تحديد عناصر"</string> + <string name="select_album" msgid="4632641262236697235">"تحديد ألبومات"</string> + <string name="select_group" msgid="9090385962030340391">"تحديد مجموعات"</string> + <string name="set_image" msgid="2331476809308010401">"تعيين الصورة كـ"</string> + <string name="wallpaper" msgid="9222901738515471972">"جارٍ إعداد الخلفية، الرجاء الانتظار..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"الخلفية"</string> + <string name="delete" msgid="2839695998251824487">"حذف"</string> + <string name="confirm_delete" msgid="5731757674837098707">"تأكيد الحذف"</string> + <string name="cancel" msgid="3637516880917356226">"إلغاء"</string> + <string name="share" msgid="3619042788254195341">"مشاركة"</string> + <string name="select_all" msgid="8623593677101437957">"تحديد الكل"</string> + <string name="deselect_all" msgid="7397531298370285581">"إلغاء تحديد الكل"</string> + <string name="slideshow" msgid="4355906903247112975">"عرض الشرائح"</string> + <string name="details" msgid="8415120088556445230">"التفاصيل"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"تبديل إلى الكاميرا"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"عرض على الخريطة"</string> + <string name="rotate_left" msgid="7412075232752726934">"تدوير لليسار"</string> + <string name="rotate_right" msgid="7340681085011826618">"تدوير لليمين"</string> + <string name="no_such_item" msgid="3161074758669642065">"لم يتم العثور على العنصر"</string> + <string name="edit" msgid="1502273844748580847">"تعديل"</string> + <string name="activity_not_found" msgid="3731390759313019518">"ليس هناك تطبيق متوفر"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"معالجة طلبات التخزين المؤقت"</string> + <string name="caching_label" msgid="3244800874547101776">"جارٍ التخزين المؤقت..."</string> + <string name="crop" msgid="7970750655414797277">"اقتصاص"</string> + <string name="set_as" msgid="3636764710790507868">"تعيين كـ"</string> + <string name="video_err" msgid="7917736494827857757">"يتعذر تشغيل الفيديو"</string> + <string name="group_by_location" msgid="316641628989023253">"بحسب الموقع"</string> + <string name="group_by_time" msgid="9046168567717963573">"بحسب الوقت"</string> + <string name="group_by_tags" msgid="3568731317210676160">"بحسب العلامات"</string> + <string name="group_by_faces" msgid="1566351636227274906">"بحسب الأشخاص"</string> + <string name="group_by_album" msgid="1532818636053818958">"بحسب الألبوم"</string> + <string name="group_by_size" msgid="153766174950394155">"بحسب الحجم"</string> + <string name="untagged" msgid="7281481064509590402">"بلا علامات"</string> + <string name="no_location" msgid="2036710947563713111">"لا موقع"</string> + <string name="show_images_only" msgid="7263218480867672653">"الصور فقط"</string> + <string name="show_videos_only" msgid="3850394623678871697">"مقاطع الفيديو فقط"</string> + <string name="show_all" msgid="4780647751652596980">"الصور ومقاطع الفيديو"</string> + <string name="appwidget_title" msgid="6410561146863700411">"معرض الصور"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"ليست هناك أية صور."</string> + <string name="crop_saved" msgid="4684933379430649946">"لقد تم حفظ الصورة التي تم اقتصاصها في التنزيل"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"لم يتم حفظ الصورة التي تم اقتصاصها"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"لا تتوفر أية ألبومات"</string> + <string name="empty_album" msgid="6307897398825514762">"لا تتوفر أية صور/مقاطع فيديو"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"ألبومات الويب بيكاسا"</string> + <string name="picasa_posts" msgid="1055151689217481993">"نبضات Google"</string> + <string name="make_available_offline" msgid="5157950985488297112">"جعلها متاحة في وضع عدم الاتصال"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"تم"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d من %2$d من العناصر:"</string> + <string name="title" msgid="7622928349908052569">"العنوان"</string> + <string name="description" msgid="3016729318096557520">"الوصف"</string> + <string name="time" msgid="1367953006052876956">"الوقت"</string> + <string name="location" msgid="3432705876921618314">"الموقع"</string> + <string name="path" msgid="4725740395885105824">"المسار"</string> + <string name="width" msgid="9215847239714321097">"العرض"</string> + <string name="height" msgid="3648885449443787772">"الارتفاع"</string> + <string name="orientation" msgid="4958327983165245513">"الاتجاه"</string> + <string name="duration" msgid="8160058911218541616">"المدة"</string> + <string name="mimetype" msgid="3518268469266183548">"النوع MIME"</string> + <string name="file_size" msgid="4670384449129762138">"حجم الملف"</string> + <string name="maker" msgid="7921835498034236197">"منشئ الوسائط"</string> + <string name="model" msgid="8240207064064337366">"الطراز"</string> + <string name="flash" msgid="2816779031261147723">"الفلاش"</string> + <string name="aperture" msgid="5920657630303915195">"فتحة العدسة"</string> + <string name="focal_length" msgid="1291383769749877010">"البعد البؤري"</string> + <string name="white_balance" msgid="8122534414851280901">"توازن اللون الأبيض"</string> + <string name="exposure_time" msgid="3146642210127439553">"مدة التعرض للضوء"</string> + <string name="iso" msgid="5028296664327335940">"سرعة ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"ملليمتر"</string> + <string name="manual" msgid="6608905477477607865">"يدوية"</string> + <string name="auto" msgid="4296941368722892821">"تلقائية"</string> + <string name="flash_on" msgid="7891556231891837284">"تم تشغيل الفلاش"</string> + <string name="flash_off" msgid="1445443413822680010">"بلا فلاش"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"جارٍ جعل الألبوم متاحًا في وضع عدم الاتصال."</item> + <item quantity="other" msgid="6929905722448632886">"جارٍ جعل الألبومات متاحة في وضع عدم الاتصال."</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"هذا العنصر مخزن محليًا ومتاح في وضع عدم الاتصال."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"كل الألبومات"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"الألبومات المحلية"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"أجهزة MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"ألبومات الويب بيكاسا"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> متوفرة"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> أو أقل"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> أو أكثر"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> إلى <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"استيراد"</string> + <string name="import_complete" msgid="1098450310074640619">"انتهى الاستيراد"</string> + <string name="import_fail" msgid="5205927625132482529">"أخفق الاستيراد"</string> + <string name="camera_connected" msgid="6984353643349303075">"الكاميرا متصلة"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"الكاميرا غير متصلة"</string> + <string name="click_import" msgid="6407959065464291972">"المس هنا للاستيراد"</string> + <string name="widget_type_album" msgid="3245149644830731121">"صور من ألبوم"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"ترتيب عشوائي لجميع الصور"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"اختيار صورة"</string> + <string name="widget_type" msgid="7308564524449340985">"نوع الأداة"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"عرض الشرائح"</string> + <string name="cache_status_title" msgid="8414708919928621485">"الجلب المسبق لصور بيكاسا:"</string> + <string name="cache_status" msgid="7690438435538533106">"تنزيل <xliff:g id="NUMBER_0">%1$s</xliff:g> من إجمالي <xliff:g id="NUMBER_1">%2$s</xliff:g> من الصور"</string> + <string name="cache_done" msgid="9194449192869777483">"اكتمل التنزيل"</string> + <string name="albums" msgid="7320787705180057947">"ألبومات"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"المواقع"</string> + <string name="people" msgid="4114003823747292747">"الأشخاص"</string> + <string name="tags" msgid="5539648765482935955">"العلامات"</string> + <string name="group_by" msgid="4308299657902209357">"تجميع بحسب"</string> + <string name="settings" msgid="1534847740615665736">"الإعدادات"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"إعدادات الحساب"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"إعدادات استخدام البيانات"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"تحميل تلقائي"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"إعدادات أخرى"</string> + <string name="about_gallery" msgid="8667445445883757255">"حول المعرض"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"مزامنة على شبكة WiFi فقط"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"تحميل جميع الصور ومقاطع الفيديو التي تلتقطها إلى ألبوم ويب بيكاسا خاص تلقائيًا"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"تمكين التحميل التلقائي"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"مزامنة صور Google قيد التشغيل"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"مزامنة صور Google قيد الإيقاف"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"تغيير تفضيلات المزامنة أو إزالة الحساب"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"عرض صور ومقاطع فيديو من هذا الحساب في المعرض"</string> + <string name="add_account" msgid="4271217504968243974">"إضافة حساب"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"اختيار حساب تحميل تلقائي"</string> +</resources> diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml new file mode 100644 index 000000000..a61ee8bd6 --- /dev/null +++ b/res/values-bg/strings.xml @@ -0,0 +1,173 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Галерия"</string> + <string name="gadget_title" msgid="259405922673466798">"Рамка на снимка"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Видеоплейър"</string> + <string name="loading_video" msgid="4013492720121891585">"Видеоклипът се зарежда..."</string> + <string name="loading_image" msgid="1200894415793838191">"Изображението се зарежда..."</string> + <string name="loading_account" msgid="928195413034552034">"Профилът се зарежда???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Възобновяване на видеоклип"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Да продължи ли възпроизвеждането от %s ?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Продължаване"</string> + <string name="loading" msgid="7038208555304563571">"Зарежда се…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"Зареждането не бе успешно"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Няма миниизображение"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Стартиране отначало"</string> + <string name="crop_save_text" msgid="8821167985419282305">"OK"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Докоснете лице за начало."</string> + <string name="saving_image" msgid="7270334453636349407">"Снимката се запазва..."</string> + <string name="crop_label" msgid="521114301871349328">"Подрязване на снимка"</string> + <string name="select_image" msgid="7841406150484742140">"Избиране на снимка"</string> + <string name="select_video" msgid="4859510992798615076">"Избиране на видеоклип"</string> + <string name="select_item" msgid="2257529413100472599">"Изберете елемент/и"</string> + <string name="select_album" msgid="4632641262236697235">"Изберете албум/и"</string> + <string name="select_group" msgid="9090385962030340391">"Изберете група/групи"</string> + <string name="set_image" msgid="2331476809308010401">"Задаване на снимката като"</string> + <string name="wallpaper" msgid="9222901738515471972">"Тапетът се задава. Моля, изчакайте..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Тапет"</string> + <string name="delete" msgid="2839695998251824487">"Изтриване"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Потвърждение на изтриване"</string> + <string name="cancel" msgid="3637516880917356226">"Отказ"</string> + <string name="share" msgid="3619042788254195341">"Споделяне"</string> + <string name="select_all" msgid="8623593677101437957">"Избиране на всичко"</string> + <string name="deselect_all" msgid="7397531298370285581">"Премахване на избора"</string> + <string name="slideshow" msgid="4355906903247112975">"Слайдшоу"</string> + <string name="details" msgid="8415120088556445230">"Подробности"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Превключване към камера"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"Показване на карта"</string> + <string name="rotate_left" msgid="7412075232752726934">"Завъртане наляво"</string> + <string name="rotate_right" msgid="7340681085011826618">"Завъртане надясно"</string> + <string name="no_such_item" msgid="3161074758669642065">"Елементът не е намерен"</string> + <string name="edit" msgid="1502273844748580847">"Редактиране"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Няма налично приложение"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Заявките за кеширане се обработват"</string> + <string name="caching_label" msgid="3244800874547101776">"Кешират се..."</string> + <string name="crop" msgid="7970750655414797277">"Подрязване"</string> + <string name="set_as" msgid="3636764710790507868">"Задаване като"</string> + <string name="video_err" msgid="7917736494827857757">"Видеоклипът не може да бъде възпроизведен"</string> + <string name="group_by_location" msgid="316641628989023253">"По местоположение"</string> + <string name="group_by_time" msgid="9046168567717963573">"По време"</string> + <string name="group_by_tags" msgid="3568731317210676160">"По маркери"</string> + <string name="group_by_faces" msgid="1566351636227274906">"По хора"</string> + <string name="group_by_album" msgid="1532818636053818958">"По албум"</string> + <string name="group_by_size" msgid="153766174950394155">"По размер"</string> + <string name="untagged" msgid="7281481064509590402">"Немаркирани"</string> + <string name="no_location" msgid="2036710947563713111">"Няма местоположение"</string> + <string name="show_images_only" msgid="7263218480867672653">"Само изображения"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Само видеоклипове"</string> + <string name="show_all" msgid="4780647751652596980">"Изображения и видеоклипове"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Фотогалерия"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Няма снимки"</string> + <string name="crop_saved" msgid="4684933379430649946">"Подрязаното изображение бе запазено в „Изтегляния“"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Подрязаното изображение не е запазено"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Няма налични албуми"</string> + <string name="empty_album" msgid="6307897398825514762">"Няма налични изображения/видеоклипове"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa Уеб Албуми"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Налице офлайн"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Готово"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d от %2$d елемента:"</string> + <string name="title" msgid="7622928349908052569">"Заглавие"</string> + <string name="description" msgid="3016729318096557520">"Описание"</string> + <string name="time" msgid="1367953006052876956">"Час"</string> + <string name="location" msgid="3432705876921618314">"Местоположение"</string> + <string name="path" msgid="4725740395885105824">"Път"</string> + <string name="width" msgid="9215847239714321097">"Ширина"</string> + <string name="height" msgid="3648885449443787772">"Височина"</string> + <string name="orientation" msgid="4958327983165245513">"Ориентация"</string> + <string name="duration" msgid="8160058911218541616">"Времетраене"</string> + <string name="mimetype" msgid="3518268469266183548">"Тип MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Файлов размер"</string> + <string name="maker" msgid="7921835498034236197">"Автор"</string> + <string name="model" msgid="8240207064064337366">"Модел"</string> + <string name="flash" msgid="2816779031261147723">"Светкавица"</string> + <string name="aperture" msgid="5920657630303915195">"Бленда"</string> + <string name="focal_length" msgid="1291383769749877010">"Дълж. на фокус"</string> + <string name="white_balance" msgid="8122534414851280901">"Бал. на бялото"</string> + <string name="exposure_time" msgid="3146642210127439553">"Експонация"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"мм"</string> + <string name="manual" msgid="6608905477477607865">"Ръчно"</string> + <string name="auto" msgid="4296941368722892821">"Авт."</string> + <string name="flash_on" msgid="7891556231891837284">"Със светкавица"</string> + <string name="flash_off" msgid="1445443413822680010">"Без светкавица"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Албумът става достъпен офлайн"</item> + <item quantity="other" msgid="6929905722448632886">"Албумите стават достъпни офлайн"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Този елемент се съхранява локално и е налице офлайн."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Всички албуми"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Местни албуми"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"MTP устройства"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Албуми в Picasa"</string> + <string name="free_space_format" msgid="8766337315709161215">"Свободни: <xliff:g id="BYTES">%s</xliff:g>"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> или по-малко"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> или повече"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> до <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Импортиране"</string> + <string name="import_complete" msgid="1098450310074640619">"Успешно импортирано"</string> + <string name="import_fail" msgid="5205927625132482529">"Импортирането не бе успешно"</string> + <string name="camera_connected" msgid="6984353643349303075">"Камерата е включена"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Камерата е изключена"</string> + <string name="click_import" msgid="6407959065464291972">"Докоснете тук, за да импортирате"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Изображения от албум"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Разбъркване на всички"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Избиране на изображение"</string> + <string name="widget_type" msgid="7308564524449340985">"Тип приспособление"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Слайдшоу"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Предварит. извличане на снимки в Picasa:"</string> + <string name="cache_status" msgid="7690438435538533106">"Изтегляне на <xliff:g id="NUMBER_0">%1$s</xliff:g> от <xliff:g id="NUMBER_1">%2$s</xliff:g> снимки"</string> + <string name="cache_done" msgid="9194449192869777483">"Изтеглянето завърши"</string> + <string name="albums" msgid="7320787705180057947">"Албуми"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"Mестопол."</string> + <string name="people" msgid="4114003823747292747">"Хора"</string> + <string name="tags" msgid="5539648765482935955">"Маркери"</string> + <string name="group_by" msgid="4308299657902209357">"Групиране по"</string> + <!-- no translation found for settings (1534847740615665736) --> + <skip /> + <string name="prefs_accounts" msgid="7942761992713671670">"Настройки на профила"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Настройки за използване на данни"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Автоматично качване"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Други настройки"</string> + <string name="about_gallery" msgid="8667445445883757255">"Всичко за галерията"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Синхронизиране само при WiFi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Автоматично качване на всички направени от вас снимки и видеоклипове в частен уеб албум в Picasa"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Активиране на автоматичното качване"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Снимки в Google: Вкл. синхрон"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Снимки в Google: Изкл. синхрон"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Промяна на синхрона / премахв. на профила"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Преглед на снимки и видеоклипове от този профил в галерията"</string> + <string name="add_account" msgid="4271217504968243974">"Добавяне на профил"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Профил за авт. качване: Избор"</string> +</resources> diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml new file mode 100644 index 000000000..06bae77d0 --- /dev/null +++ b/res/values-ca/strings.xml @@ -0,0 +1,178 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galeria"</string> + <string name="gadget_title" msgid="259405922673466798">"Marc de la imatge"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Reproductor de vídeo"</string> + <string name="loading_video" msgid="4013492720121891585">"S\'està carregant el vídeo..."</string> + <string name="loading_image" msgid="1200894415793838191">"S\'està carregant la imatge…"</string> + <string name="loading_account" msgid="928195413034552034">"S\'està carregant el compte???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Reprèn el vídeo"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Voleu reprendre la reproducció a partir de %s?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Reprèn la reproducció"</string> + <string name="loading" msgid="7038208555304563571">"S\'està carregant…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"S\'ha produït un error en carregar"</string> + <string name="no_thumbnail" msgid="284723185546429750">"No hi ha cap miniatura"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Torna a començar"</string> + <string name="crop_save_text" msgid="8821167985419282305">"D\'acord"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Piqueu en una cara per començar."</string> + <string name="saving_image" msgid="7270334453636349407">"S\'està desant la imatge..."</string> + <string name="crop_label" msgid="521114301871349328">"Escapça la imatge"</string> + <string name="select_image" msgid="7841406150484742140">"Selecciona una foto"</string> + <string name="select_video" msgid="4859510992798615076">"Selecciona un vídeo"</string> + <string name="select_item" msgid="2257529413100472599">"Selecciona elements"</string> + <string name="select_album" msgid="4632641262236697235">"Selecciona àlbums"</string> + <string name="select_group" msgid="9090385962030340391">"Selecciona grups"</string> + <string name="set_image" msgid="2331476809308010401">"Defineix la imatge com a"</string> + <string name="wallpaper" msgid="9222901738515471972">"S\'està definint el fons de pantalla…"</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Fons de pantalla"</string> + <string name="delete" msgid="2839695998251824487">"Suprimeix"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Confirmeu la supressió"</string> + <string name="cancel" msgid="3637516880917356226">"Cancel·la"</string> + <string name="share" msgid="3619042788254195341">"Comparteix"</string> + <string name="select_all" msgid="8623593677101437957">"Selecciona-ho tot"</string> + <string name="deselect_all" msgid="7397531298370285581">"Anul·la la selecció de tot"</string> + <string name="slideshow" msgid="4355906903247112975">"Presentació de diapositives"</string> + <string name="details" msgid="8415120088556445230">"Detalls"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Canvia a la càmera"</string> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"%1$d seleccionats"</item> + <item quantity="one" msgid="2478365152745637768">"%1$d seleccionat"</item> + <item quantity="other" msgid="754722656147810487">"%1$d seleccionats"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"%1$d seleccionats"</item> + <item quantity="one" msgid="6184377003099987825">"%1$d seleccionat"</item> + <item quantity="other" msgid="53105607141906130">"%1$d seleccionats"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"%1$d seleccionats"</item> + <item quantity="one" msgid="5030162638216034260">"%1$d seleccionat"</item> + <item quantity="other" msgid="3512041363942842738">"%1$d seleccionats"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"Mostra al mapa"</string> + <string name="rotate_left" msgid="7412075232752726934">"Gira a l\'esquerra"</string> + <string name="rotate_right" msgid="7340681085011826618">"Gira a la dreta"</string> + <string name="no_such_item" msgid="3161074758669642065">"No s\'ha trobat l\'element"</string> + <string name="edit" msgid="1502273844748580847">"Edita"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Cap aplicació disponible"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Processa les sol·licituds de memòria cau"</string> + <string name="caching_label" msgid="3244800874547101776">"S\'està desant a la memòria cau..."</string> + <string name="crop" msgid="7970750655414797277">"Escapça"</string> + <string name="set_as" msgid="3636764710790507868">"Defineix com a"</string> + <string name="video_err" msgid="7917736494827857757">"No es pot reproduir el vídeo"</string> + <string name="group_by_location" msgid="316641628989023253">"Per ubicació"</string> + <string name="group_by_time" msgid="9046168567717963573">"Per temps"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Per etiquetes"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Per persones"</string> + <string name="group_by_album" msgid="1532818636053818958">"Per àlbum"</string> + <string name="group_by_size" msgid="153766174950394155">"Per mida"</string> + <string name="untagged" msgid="7281481064509590402">"Sense etiquetar"</string> + <string name="no_location" msgid="2036710947563713111">"Sense ubicació"</string> + <string name="show_images_only" msgid="7263218480867672653">"Només imatges"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Només vídeos"</string> + <string name="show_all" msgid="4780647751652596980">"Imatges i vídeos"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Galeria de fotos"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"No hi ha cap foto"</string> + <string name="crop_saved" msgid="4684933379430649946">"La imatge retallada s\'ha desat a la baixada"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"La imatge retallada no s\'ha desat"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"No hi ha cap àlbum disponible"</string> + <string name="empty_album" msgid="6307897398825514762">"No hi ha imatges/vídeos disponibles"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Àlbums web de Picasa"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Disponible fora de línia"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Fet"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d de %2$d elements:"</string> + <string name="title" msgid="7622928349908052569">"Títol"</string> + <string name="description" msgid="3016729318096557520">"Descripció"</string> + <string name="time" msgid="1367953006052876956">"Hora"</string> + <string name="location" msgid="3432705876921618314">"Ubicació"</string> + <string name="path" msgid="4725740395885105824">"Camí"</string> + <string name="width" msgid="9215847239714321097">"Amplada"</string> + <string name="height" msgid="3648885449443787772">"Alçada"</string> + <string name="orientation" msgid="4958327983165245513">"Orientació"</string> + <string name="duration" msgid="8160058911218541616">"Durada"</string> + <string name="mimetype" msgid="3518268469266183548">"Tipus MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Mida del fitxer"</string> + <string name="maker" msgid="7921835498034236197">"Creador"</string> + <string name="model" msgid="8240207064064337366">"Model"</string> + <string name="flash" msgid="2816779031261147723">"Flaix"</string> + <string name="aperture" msgid="5920657630303915195">"Obertura"</string> + <string name="focal_length" msgid="1291383769749877010">"Longitud focal"</string> + <string name="white_balance" msgid="8122534414851280901">"Balanç de blancs"</string> + <string name="exposure_time" msgid="3146642210127439553">"Temps d\'exposició"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Manual"</string> + <string name="auto" msgid="4296941368722892821">"Automàtic"</string> + <string name="flash_on" msgid="7891556231891837284">"Flaix disparat"</string> + <string name="flash_off" msgid="1445443413822680010">"Sense flaix"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Fent que l\'àlbum estigui disponible fora de línia"</item> + <item quantity="other" msgid="6929905722448632886">"Fent que àlbums estiguin disponibles fora de línia"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"L\'element s\'ha emmagatzemat localment i està disponible fora de línia."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Tots els àlbums"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Àlbums locals"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"Dispositius MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Àlbums de Picasa"</string> + <string name="free_space_format" msgid="8766337315709161215">"Lliure: <xliff:g id="BYTES">%s</xliff:g>"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> o menys"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> o més"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> a <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Importa"</string> + <string name="import_complete" msgid="1098450310074640619">"Import. completada"</string> + <string name="import_fail" msgid="5205927625132482529">"Error d\'importació"</string> + <string name="camera_connected" msgid="6984353643349303075">"S\'ha connectat la càmera"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"S\'ha desconnectat la càmera"</string> + <string name="click_import" msgid="6407959065464291972">"Toca aquí per importar"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Imatges d\'un àlbum"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Barreja totes les imatges"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Tria una imatge"</string> + <string name="widget_type" msgid="7308564524449340985">"Tipus de widget"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Pres. diapositives"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Obtenció prèvia de fotos de Picasa:"</string> + <string name="cache_status" msgid="7690438435538533106">"Baixada de <xliff:g id="NUMBER_0">%1$s</xliff:g> de <xliff:g id="NUMBER_1">%2$s</xliff:g> fotos"</string> + <string name="cache_done" msgid="9194449192869777483">"Baixada completada"</string> + <string name="albums" msgid="7320787705180057947">"Àlbums"</string> + <string name="times" msgid="2023033894889499219">"Vegades"</string> + <string name="locations" msgid="6649297994083130305">"Ubicacions"</string> + <string name="people" msgid="4114003823747292747">"Persones"</string> + <string name="tags" msgid="5539648765482935955">"Etiquetes"</string> + <string name="group_by" msgid="4308299657902209357">"Agrupa per"</string> + <!-- no translation found for settings (1534847740615665736) --> + <skip /> + <string name="prefs_accounts" msgid="7942761992713671670">"Configuració del compte"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Configuració de l\'ús de dades"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Càrrega automàtica"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Altres paràmetres de configuració"</string> + <string name="about_gallery" msgid="8667445445883757255">"Quant a la Galeria"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Sincronització només amb connexió Wi-Fi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Penja automàticament totes les fotos i els vídeos que facis a un àlbum privat d\'Àlbums web de Picasa"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Activa la càrrega automàtica"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Sincron. fotos Google ACTIVADA"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Sinc. fotos Google DESACTIVADA"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Canvia les pref. de sinc. o elimina aquest compte"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Visualitza fotos i vídeos d\'aquest compte a la Galeria"</string> + <string name="add_account" msgid="4271217504968243974">"Afegeix un compte"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Tria compte càrrega automàtica"</string> +</resources> diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml new file mode 100644 index 000000000..a3bb4047d --- /dev/null +++ b/res/values-cs/strings.xml @@ -0,0 +1,177 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galerie"</string> + <string name="gadget_title" msgid="259405922673466798">"Rámeček fotografie"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Přehrávač videa"</string> + <string name="loading_video" msgid="4013492720121891585">"Načítání videa..."</string> + <string name="loading_image" msgid="1200894415793838191">"Načítání obrázku..."</string> + <string name="loading_account" msgid="928195413034552034">"Načítání účtu???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Pokračovat v přehrávání videa"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Pokračovat v přehrávání od %s?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Pokračovat v přehrávání"</string> + <string name="loading" msgid="7038208555304563571">"Načítání..."</string> + <string name="fail_to_load" msgid="2710120770735315683">"Načtení se nezdařilo"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Miniatura není dostupná"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Začít znovu"</string> + <string name="crop_save_text" msgid="8821167985419282305">"OK"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Začněte klepnutím na obličej."</string> + <string name="saving_image" msgid="7270334453636349407">"Ukládání fotografie..."</string> + <string name="crop_label" msgid="521114301871349328">"Oříznout fotografii"</string> + <string name="select_image" msgid="7841406150484742140">"Vyberte fotografii"</string> + <string name="select_video" msgid="4859510992798615076">"Vyberte video"</string> + <string name="select_item" msgid="2257529413100472599">"Vyberte položky"</string> + <string name="select_album" msgid="4632641262236697235">"Vyberte alba"</string> + <string name="select_group" msgid="9090385962030340391">"Vyberte skupiny"</string> + <string name="set_image" msgid="2331476809308010401">"Fotografie bude použita jako"</string> + <string name="wallpaper" msgid="9222901738515471972">"Nastavování tapety, čekejte prosím..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Tapeta"</string> + <string name="delete" msgid="2839695998251824487">"Smazat"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Potvrdit smazání"</string> + <string name="cancel" msgid="3637516880917356226">"Zrušit"</string> + <string name="share" msgid="3619042788254195341">"Sdílet"</string> + <string name="select_all" msgid="8623593677101437957">"Vybrat vše"</string> + <string name="deselect_all" msgid="7397531298370285581">"Zrušit výběr všech"</string> + <string name="slideshow" msgid="4355906903247112975">"Prezentace"</string> + <string name="details" msgid="8415120088556445230">"Podrobnosti"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Přepnout do režimu Fotoaparát"</string> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"Vybráno: %1$d"</item> + <item quantity="one" msgid="2478365152745637768">"Vybráno: %1$d"</item> + <item quantity="other" msgid="754722656147810487">"Vybráno: %1$d"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"Vybráno: %1$d"</item> + <item quantity="one" msgid="6184377003099987825">"Vybráno: %1$d"</item> + <item quantity="other" msgid="53105607141906130">"Vybráno: %1$d"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"Vybráno: %1$d"</item> + <item quantity="one" msgid="5030162638216034260">"Vybráno: %1$d"</item> + <item quantity="other" msgid="3512041363942842738">"Vybráno: %1$d"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"Zobrazit na mapě"</string> + <string name="rotate_left" msgid="7412075232752726934">"Otočit doleva"</string> + <string name="rotate_right" msgid="7340681085011826618">"Otočit doprava"</string> + <string name="no_such_item" msgid="3161074758669642065">"Položka nenalezena"</string> + <string name="edit" msgid="1502273844748580847">"Upravit"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Nejsou k dispozici žádné aplikace"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Zpracování požadavků na uložení do mezipaměti"</string> + <string name="caching_label" msgid="3244800874547101776">"Mezipaměť..."</string> + <string name="crop" msgid="7970750655414797277">"Oříznout"</string> + <string name="set_as" msgid="3636764710790507868">"Nastavit jako"</string> + <string name="video_err" msgid="7917736494827857757">"Video nelze přehrát"</string> + <string name="group_by_location" msgid="316641628989023253">"Podle místa"</string> + <string name="group_by_time" msgid="9046168567717963573">"Podle času"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Podle tagů"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Podle osob"</string> + <string name="group_by_album" msgid="1532818636053818958">"Podle alba"</string> + <string name="group_by_size" msgid="153766174950394155">"Podle velikosti"</string> + <string name="untagged" msgid="7281481064509590402">"Neoznačeno"</string> + <string name="no_location" msgid="2036710947563713111">"Žádná poloha"</string> + <string name="show_images_only" msgid="7263218480867672653">"Pouze obrázky"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Pouze videa"</string> + <string name="show_all" msgid="4780647751652596980">"Obrázky a videa"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Galerie fotografií"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Žádné fotografie"</string> + <string name="crop_saved" msgid="4684933379430649946">"Ořezaný snímek byl uložen do slož. staž. souborů"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Ořezaný snímek není uložen"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Nejsou k dispozici žádná alba"</string> + <string name="empty_album" msgid="6307897398825514762">"Nejsou k dispozici žádné obrázky ani videa"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Webová alba Picasa"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Zpřístupnit offline"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Hotovo"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d z %2$d položek:"</string> + <string name="title" msgid="7622928349908052569">"Název"</string> + <string name="description" msgid="3016729318096557520">"Popis"</string> + <string name="time" msgid="1367953006052876956">"Čas"</string> + <string name="location" msgid="3432705876921618314">"Místo"</string> + <string name="path" msgid="4725740395885105824">"Cesta"</string> + <string name="width" msgid="9215847239714321097">"Šířka"</string> + <string name="height" msgid="3648885449443787772">"Výška"</string> + <string name="orientation" msgid="4958327983165245513">"Orientace"</string> + <string name="duration" msgid="8160058911218541616">"Délka"</string> + <string name="mimetype" msgid="3518268469266183548">"Typ MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Velikost souboru"</string> + <string name="maker" msgid="7921835498034236197">"Tvůrce"</string> + <string name="model" msgid="8240207064064337366">"Model"</string> + <string name="flash" msgid="2816779031261147723">"Blesk"</string> + <string name="aperture" msgid="5920657630303915195">"Clona"</string> + <string name="focal_length" msgid="1291383769749877010">"Ohnisk. vzdál."</string> + <string name="white_balance" msgid="8122534414851280901">"Vyvážení bílé"</string> + <string name="exposure_time" msgid="3146642210127439553">"Doba expozice"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Ručně"</string> + <string name="auto" msgid="4296941368722892821">"Autom."</string> + <string name="flash_on" msgid="7891556231891837284">"S bleskem"</string> + <string name="flash_off" msgid="1445443413822680010">"Bez blesku"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Zpřístupnění alba offline"</item> + <item quantity="other" msgid="6929905722448632886">"Zpřístupnění alb offline"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Tato položka je uložena v místním úložišti a je k dispozici offline."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Všechna alba"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Místní alba"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"Zařízení MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Alba Picasa"</string> + <string name="free_space_format" msgid="8766337315709161215">"Volná paměť: <xliff:g id="BYTES">%s</xliff:g>"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> nebo menší"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> nebo větší"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> až <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Importovat"</string> + <string name="import_complete" msgid="1098450310074640619">"Import byl dokončen"</string> + <string name="import_fail" msgid="5205927625132482529">"Import se nezdařil"</string> + <string name="camera_connected" msgid="6984353643349303075">"Fotoaparát byl připojen"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Fotoaparát byl odpojen"</string> + <string name="click_import" msgid="6407959065464291972">"Chcete-li zahájit import, dotkněte se zde"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Obrázky z alba"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Náhodně všechny obrázky"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Vybrat obrázek"</string> + <string name="widget_type" msgid="7308564524449340985">"Typ widgetu"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Prezentace"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Předběžné načítání fotografií Picasa:"</string> + <string name="cache_status" msgid="7690438435538533106">"Stáhnout <xliff:g id="NUMBER_0">%1$s</xliff:g> z <xliff:g id="NUMBER_1">%2$s</xliff:g> fotografií"</string> + <string name="cache_done" msgid="9194449192869777483">"Stahování bylo dokončeno"</string> + <string name="albums" msgid="7320787705180057947">"Alba"</string> + <string name="times" msgid="2023033894889499219">"Časy"</string> + <string name="locations" msgid="6649297994083130305">"Lokality"</string> + <string name="people" msgid="4114003823747292747">"Lidé"</string> + <string name="tags" msgid="5539648765482935955">"Tagy"</string> + <string name="group_by" msgid="4308299657902209357">"Seskupit podle"</string> + <string name="settings" msgid="1534847740615665736">"Nastavení"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"Nastavení účtu"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Nastavení využití dat"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Automatické nahrávání"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Další nastavení"</string> + <string name="about_gallery" msgid="8667445445883757255">"O Galerii"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Synchronizovat pouze v síti Wi-Fi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Automaticky nahrát všechny pořízené fotky a videa do soukromého Webového alba Picasa"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Povolit automatické nahrávání"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Synchronizace fotografií: ZAP."</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Synchronizace fotografií: VYP."</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Změna nastavení synch. či odebrání účtu"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Zobrazit fotografie a videa z tohoto účtu v Galerii"</string> + <string name="add_account" msgid="4271217504968243974">"Přidat účet"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Výběr účtu pro autom. nahrání"</string> +</resources> diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml new file mode 100644 index 000000000..eceb380c7 --- /dev/null +++ b/res/values-da/strings.xml @@ -0,0 +1,172 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galleri"</string> + <string name="gadget_title" msgid="259405922673466798">"Billedramme"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Videoafspiller"</string> + <string name="loading_video" msgid="4013492720121891585">"Indlæser video ..."</string> + <string name="loading_image" msgid="1200894415793838191">"Indlæser billede..."</string> + <string name="loading_account" msgid="928195413034552034">"Indlæser konto???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Genoptag video"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Genoptag afspilning fra %s ?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Genoptag afspilning"</string> + <string name="loading" msgid="7038208555304563571">"Indlæser..."</string> + <string name="fail_to_load" msgid="2710120770735315683">"Blev ikke indlæst"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Ingen miniature"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Start igen"</string> + <string name="crop_save_text" msgid="8821167985419282305">"OK"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Tryk på et ansigt for at begynde."</string> + <string name="saving_image" msgid="7270334453636349407">"Gemmer billede ..."</string> + <string name="crop_label" msgid="521114301871349328">"Beskær billede"</string> + <string name="select_image" msgid="7841406150484742140">"Vælg foto"</string> + <string name="select_video" msgid="4859510992798615076">"Vælg video"</string> + <string name="select_item" msgid="2257529413100472599">"Vælg element(er)"</string> + <string name="select_album" msgid="4632641262236697235">"Vælg album(mer)"</string> + <string name="select_group" msgid="9090385962030340391">"Vælg gruppe(r)"</string> + <string name="set_image" msgid="2331476809308010401">"Angiv billedet som"</string> + <string name="wallpaper" msgid="9222901738515471972">"Angiver tapet. Vent et øjeblik ..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Tapet"</string> + <string name="delete" msgid="2839695998251824487">"Slet"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Bekræft sletning"</string> + <string name="cancel" msgid="3637516880917356226">"Annuller"</string> + <string name="share" msgid="3619042788254195341">"Del"</string> + <string name="select_all" msgid="8623593677101437957">"Vælg alle"</string> + <string name="deselect_all" msgid="7397531298370285581">"Fravælg alle"</string> + <string name="slideshow" msgid="4355906903247112975">"Diasshow"</string> + <string name="details" msgid="8415120088556445230">"Detaljer"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Skift til kamera"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"Vis på kort"</string> + <string name="rotate_left" msgid="7412075232752726934">"Roter til venstre"</string> + <string name="rotate_right" msgid="7340681085011826618">"Roter til højre"</string> + <string name="no_such_item" msgid="3161074758669642065">"Elementet blev ikke fundet"</string> + <string name="edit" msgid="1502273844748580847">"Rediger"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Ingen tilgængelig applikation"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Håndter anmodning om cachelagring"</string> + <string name="caching_label" msgid="3244800874547101776">"Caching..."</string> + <string name="crop" msgid="7970750655414797277">"Beskær"</string> + <string name="set_as" msgid="3636764710790507868">"Indstil som"</string> + <string name="video_err" msgid="7917736494827857757">"Kan ikke afspille video"</string> + <string name="group_by_location" msgid="316641628989023253">"Efter placering"</string> + <string name="group_by_time" msgid="9046168567717963573">"Efter tid"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Efter tags"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Efter mennesker"</string> + <string name="group_by_album" msgid="1532818636053818958">"Efter album"</string> + <string name="group_by_size" msgid="153766174950394155">"Efter størrelse"</string> + <string name="untagged" msgid="7281481064509590402">"Utagget"</string> + <string name="no_location" msgid="2036710947563713111">"Ingen placering"</string> + <string name="show_images_only" msgid="7263218480867672653">"Kun billeder"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Kun videoer"</string> + <string name="show_all" msgid="4780647751652596980">"Billeder og videoer"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Billedgalleri"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Ingen fotos"</string> + <string name="crop_saved" msgid="4684933379430649946">"Det beskårne billede er gemt i download"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Det beskårne billede er ikke gemt"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Der er ingen tilgængelige albummer"</string> + <string name="empty_album" msgid="6307897398825514762">"Der er ingen tilgængelige billeder/videoer"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa Webalbum"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Gør tilgængelig offline"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Udført"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d ud af %2$d enheder:"</string> + <string name="title" msgid="7622928349908052569">"Titel"</string> + <string name="description" msgid="3016729318096557520">"Beskrivelse"</string> + <string name="time" msgid="1367953006052876956">"Tid"</string> + <string name="location" msgid="3432705876921618314">"Placering"</string> + <string name="path" msgid="4725740395885105824">"Sti:"</string> + <string name="width" msgid="9215847239714321097">"Bredde"</string> + <string name="height" msgid="3648885449443787772">"Højde"</string> + <string name="orientation" msgid="4958327983165245513">"Retning"</string> + <string name="duration" msgid="8160058911218541616">"Varighed"</string> + <string name="mimetype" msgid="3518268469266183548">"MIME-type"</string> + <string name="file_size" msgid="4670384449129762138">"Filstørrelse"</string> + <string name="maker" msgid="7921835498034236197">"Fremstiller"</string> + <string name="model" msgid="8240207064064337366">"Model"</string> + <string name="flash" msgid="2816779031261147723">"Blitz"</string> + <string name="aperture" msgid="5920657630303915195">"Blænderåbning"</string> + <string name="focal_length" msgid="1291383769749877010">"Fokallængde"</string> + <string name="white_balance" msgid="8122534414851280901">"Hvidbalance"</string> + <string name="exposure_time" msgid="3146642210127439553">"Eksp. tid"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Manuel"</string> + <string name="auto" msgid="4296941368722892821">"Auto"</string> + <string name="flash_on" msgid="7891556231891837284">"Blitz affyret"</string> + <string name="flash_off" msgid="1445443413822680010">"Ingen blitz"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Gør musikalbummer tilgængelige offline"</item> + <item quantity="other" msgid="6929905722448632886">"Gør musikalbummer tilgængelige offline"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Dette element er gemt lokalt og er tilgængeligt offline."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Alle albummer"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Lokale albummer"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"MTP-enheder"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Picasa-albummer"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> ledig"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> eller mindre"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> eller over"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> til <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Importer"</string> + <string name="import_complete" msgid="1098450310074640619">"Importen er fuldført"</string> + <string name="import_fail" msgid="5205927625132482529">"Importen mislykkedes"</string> + <string name="camera_connected" msgid="6984353643349303075">"Kameraet er tilkoblet"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Kameraet er frakoblet"</string> + <string name="click_import" msgid="6407959065464291972">"Tryk her for at importere"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Billeder fra et album"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Bland alle billeder"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Vælg et billede"</string> + <string name="widget_type" msgid="7308564524449340985">"Widgettype"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Diasshow"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Henter Picasa-billeder på forhånd:"</string> + <string name="cache_status" msgid="7690438435538533106">"<xliff:g id="NUMBER_0">%1$s</xliff:g> downloadet ud af <xliff:g id="NUMBER_1">%2$s</xliff:g> fotos"</string> + <string name="cache_done" msgid="9194449192869777483">"Download er fuldført"</string> + <string name="albums" msgid="7320787705180057947">"Albummer"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"Placering"</string> + <string name="people" msgid="4114003823747292747">"Personer"</string> + <string name="tags" msgid="5539648765482935955">"Tags"</string> + <string name="group_by" msgid="4308299657902209357">"Grupper efter"</string> + <string name="settings" msgid="1534847740615665736">"Indstillinger"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"Kontoindstillinger"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Indstillinger til dataforbrug"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Auto-upload"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Andre indstillinger"</string> + <string name="about_gallery" msgid="8667445445883757255">"Om galleri"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Synkroniser kun på Wi-Fi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Upload automatisk alle de billeder og videoer, du tager, til et privat Picasa-webalbum"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Aktiver auto-upload"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Synk. af Google-fotos er tændt"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Synkr. af Google-fotos er FRA"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Skift præf. for synk., eller fjern kontoen"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Se billeder og videoer fra denne konto i galleriet"</string> + <string name="add_account" msgid="4271217504968243974">"Tilføj konto"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Vælg konto til auto-upload"</string> +</resources> diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml new file mode 100644 index 000000000..49180e642 --- /dev/null +++ b/res/values-de/strings.xml @@ -0,0 +1,172 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galerie"</string> + <string name="gadget_title" msgid="259405922673466798">"Bildrahmen"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Google Video Player"</string> + <string name="loading_video" msgid="4013492720121891585">"Video wird geladen..."</string> + <string name="loading_image" msgid="1200894415793838191">"Bild wird geladen…"</string> + <string name="loading_account" msgid="928195413034552034">"Konto wird geladen..."</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Mit Video fortfahren"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Mit Wiedergabe fortfahren ab %s ?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Mit Wiedergabe fortfahren"</string> + <string name="loading" msgid="7038208555304563571">"Wird geladen..."</string> + <string name="fail_to_load" msgid="2710120770735315683">"Laden fehlgeschlagen"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Keine Miniaturansicht"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Starten"</string> + <string name="crop_save_text" msgid="8821167985419282305">"OK"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Zum Beginnen auf ein Gesicht tippen"</string> + <string name="saving_image" msgid="7270334453636349407">"Bild wird gespeichert..."</string> + <string name="crop_label" msgid="521114301871349328">"Bild zuschneiden"</string> + <string name="select_image" msgid="7841406150484742140">"Foto auswählen"</string> + <string name="select_video" msgid="4859510992798615076">"Video auswählen"</string> + <string name="select_item" msgid="2257529413100472599">"Element(e) auswählen"</string> + <string name="select_album" msgid="4632641262236697235">"Album/-en auswählen"</string> + <string name="select_group" msgid="9090385962030340391">"Gruppe(n) auswählen"</string> + <string name="set_image" msgid="2331476809308010401">"Bild festlegen als"</string> + <string name="wallpaper" msgid="9222901738515471972">"Hintergrund wird eingestellt, bitte warten..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Hintergrund"</string> + <string name="delete" msgid="2839695998251824487">"Löschen"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Löschen bestätigen"</string> + <string name="cancel" msgid="3637516880917356226">"Abbrechen"</string> + <string name="share" msgid="3619042788254195341">"Weitergeben"</string> + <string name="select_all" msgid="8623593677101437957">"Alle ausw."</string> + <string name="deselect_all" msgid="7397531298370285581">"Keine ausw."</string> + <string name="slideshow" msgid="4355906903247112975">"Diashow"</string> + <string name="details" msgid="8415120088556445230">"Details"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Zu Kamera wechseln"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"Auf Karte anzeigen"</string> + <string name="rotate_left" msgid="7412075232752726934">"Nach links drehen"</string> + <string name="rotate_right" msgid="7340681085011826618">"Nach rechts drehen"</string> + <string name="no_such_item" msgid="3161074758669642065">"Element nicht gefunden"</string> + <string name="edit" msgid="1502273844748580847">"Bearbeiten"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Keine Anwendung verfügbar"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Caching-Anfragen verarbeiten"</string> + <string name="caching_label" msgid="3244800874547101776">"Zwischenspeicherung läuft..."</string> + <string name="crop" msgid="7970750655414797277">"Zuschneiden"</string> + <string name="set_as" msgid="3636764710790507868">"Festlegen als"</string> + <string name="video_err" msgid="7917736494827857757">"Video kann nicht wiedergegeben werden."</string> + <string name="group_by_location" msgid="316641628989023253">"Nach Standort"</string> + <string name="group_by_time" msgid="9046168567717963573">"Nach Zeit"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Nach Tags"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Nach Personen"</string> + <string name="group_by_album" msgid="1532818636053818958">"Nach Album"</string> + <string name="group_by_size" msgid="153766174950394155">"Nach Größe"</string> + <string name="untagged" msgid="7281481064509590402">"Ohne Tag"</string> + <string name="no_location" msgid="2036710947563713111">"Kein Ort"</string> + <string name="show_images_only" msgid="7263218480867672653">"Nur Bilder"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Nur Videos"</string> + <string name="show_all" msgid="4780647751652596980">"Bilder und Videos"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Fotogalerie"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Keine Fotos"</string> + <string name="crop_saved" msgid="4684933379430649946">"Zugeschnittenes Bild im Download gespeichert"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Zugeschnittenes Bild nicht gespeichert"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Es sind keine Alben verfügbar."</string> + <string name="empty_album" msgid="6307897398825514762">"Es sind keine Bilder/Videos verfügbar."</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa-Webalben"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Offline bereitstellen"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Fertig"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d von %2$d Elementen:"</string> + <string name="title" msgid="7622928349908052569">"Titel"</string> + <string name="description" msgid="3016729318096557520">"Beschreibung"</string> + <string name="time" msgid="1367953006052876956">"Uhrzeit"</string> + <string name="location" msgid="3432705876921618314">"Ort"</string> + <string name="path" msgid="4725740395885105824">"Pfad"</string> + <string name="width" msgid="9215847239714321097">"Breite"</string> + <string name="height" msgid="3648885449443787772">"Höhe"</string> + <string name="orientation" msgid="4958327983165245513">"Ausrichtung"</string> + <string name="duration" msgid="8160058911218541616">"Dauer"</string> + <string name="mimetype" msgid="3518268469266183548">"MIME-Typ"</string> + <string name="file_size" msgid="4670384449129762138">"Dateigröße"</string> + <string name="maker" msgid="7921835498034236197">"Hersteller"</string> + <string name="model" msgid="8240207064064337366">"Modell"</string> + <string name="flash" msgid="2816779031261147723">"Blitz"</string> + <string name="aperture" msgid="5920657630303915195">"Blende"</string> + <string name="focal_length" msgid="1291383769749877010">"Brennweite"</string> + <string name="white_balance" msgid="8122534414851280901">"Weißabgleich"</string> + <string name="exposure_time" msgid="3146642210127439553">"Belichtungszeit"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Manuell"</string> + <string name="auto" msgid="4296941368722892821">"Autom."</string> + <string name="flash_on" msgid="7891556231891837284">"Blitz ausgelöst"</string> + <string name="flash_off" msgid="1445443413822680010">"Ohne Blitz"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Album wird offline bereitgestellt"</item> + <item quantity="other" msgid="6929905722448632886">"Alben werden offline bereitgestellt"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Dieses Element ist lokal gespeichert und offline verfügbar."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Alle Alben"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Lokale Alben"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"MTP-Geräte"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Picasa-Alben"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> verfügbar"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> oder kleiner"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> oder größer"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> bis <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Importieren"</string> + <string name="import_complete" msgid="1098450310074640619">"Import abgeschlossen"</string> + <string name="import_fail" msgid="5205927625132482529">"Fehler beim Import"</string> + <string name="camera_connected" msgid="6984353643349303075">"Kamera verbunden"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Kamera nicht verbunden"</string> + <string name="click_import" msgid="6407959065464291972">"Zum Importieren hier berühren"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Bilder aus einem Album"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Zufallsauswahl"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Bild auswählen"</string> + <string name="widget_type" msgid="7308564524449340985">"Widget-Typ"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Diashow"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Vorabruf von Picasa-Fotos:"</string> + <string name="cache_status" msgid="7690438435538533106">"<xliff:g id="NUMBER_0">%1$s</xliff:g> von <xliff:g id="NUMBER_1">%2$s</xliff:g> Fotos heruntergeladen"</string> + <string name="cache_done" msgid="9194449192869777483">"Download abgeschlossen"</string> + <string name="albums" msgid="7320787705180057947">"Alben"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"Orte"</string> + <string name="people" msgid="4114003823747292747">"Personen"</string> + <string name="tags" msgid="5539648765482935955">"Tags"</string> + <string name="group_by" msgid="4308299657902209357">"Gruppieren nach"</string> + <string name="settings" msgid="1534847740615665736">"Einstellungen"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"Kontoeinstellungen"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Datennutzungseinstellungen"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Automatischer Upload"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Weitere Einstellungen"</string> + <string name="about_gallery" msgid="8667445445883757255">"Über die Galerie"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Nur in WLAN synchronisieren"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Alle aufgenommenen Fotos und Videos automatisch in ein privates Picasa-Webalbum hochladen"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Automatischen Upload aktivieren"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Google Fotos-Synchr. ein"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Google Fotos-Synchr. aus"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Synchr.-Einstellungen ändern oder Konto entfernen"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Fotos und Videos aus diesem Konto in der Galerie anzeigen"</string> + <string name="add_account" msgid="4271217504968243974">"Konto hinzufügen"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Konto für autom. Upload wählen"</string> +</resources> diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml new file mode 100644 index 000000000..5d6c0de37 --- /dev/null +++ b/res/values-el/strings.xml @@ -0,0 +1,177 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Συλλογή"</string> + <string name="gadget_title" msgid="259405922673466798">"Πλαίσιο εικόνας"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Πρόγραμμα αναπαραγωγής βίντεο"</string> + <string name="loading_video" msgid="4013492720121891585">"Φόρτωση βίντεο..."</string> + <string name="loading_image" msgid="1200894415793838191">"Φόρτωση εικόνας…"</string> + <string name="loading_account" msgid="928195413034552034">"Φόρτωση λογαριασμού..."</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Συνέχιση βίντεο"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Συνέχιση αναπαραγωγής από το %s;"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Συνέχιση αναπαραγωγής"</string> + <string name="loading" msgid="7038208555304563571">"Φόρτωση..."</string> + <string name="fail_to_load" msgid="2710120770735315683">"Απέτυχε η φόρτωση"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Δεν υπάρχει μικρογραφία"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Έναρξη από την αρχή"</string> + <string name="crop_save_text" msgid="8821167985419282305">"OΚ"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Πατήστε σε ένα πρόσωπο για να ξεκινήσετε."</string> + <string name="saving_image" msgid="7270334453636349407">"Αποθήκευση εικόνας..."</string> + <string name="crop_label" msgid="521114301871349328">"Περικοπή εικόνας"</string> + <string name="select_image" msgid="7841406150484742140">"Επιλογή φωτογραφίας"</string> + <string name="select_video" msgid="4859510992798615076">"Επιλογή βίντεο"</string> + <string name="select_item" msgid="2257529413100472599">"Επιλογή αντικειμένων"</string> + <string name="select_album" msgid="4632641262236697235">"Επιλογή λευκωμάτων"</string> + <string name="select_group" msgid="9090385962030340391">"Επιλέξτε ομάδες"</string> + <string name="set_image" msgid="2331476809308010401">"Ορισμός εικόνας ως"</string> + <string name="wallpaper" msgid="9222901738515471972">"Ρύθμιση ταπετσαρίας, περιμένετε..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Ταπετσαρία"</string> + <string name="delete" msgid="2839695998251824487">"Διαγραφή"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Επιβεβαίωση διαγραφής"</string> + <string name="cancel" msgid="3637516880917356226">"Ακύρωση"</string> + <string name="share" msgid="3619042788254195341">"Κοινή χρήση"</string> + <string name="select_all" msgid="8623593677101437957">"Επιλογή όλων"</string> + <string name="deselect_all" msgid="7397531298370285581">"Κατάργηση επιλογής όλων"</string> + <string name="slideshow" msgid="4355906903247112975">"Προβολή διαφανειών"</string> + <string name="details" msgid="8415120088556445230">"Λεπτομέρειες"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Φωτογραφική μηχανή"</string> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"Επιλέχθηκαν %1$d"</item> + <item quantity="one" msgid="2478365152745637768">"Επιλέχθηκαν %1$d"</item> + <item quantity="other" msgid="754722656147810487">"Επιλέχθηκαν %1$d"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"Επιλέχθηκαν %1$d"</item> + <item quantity="one" msgid="6184377003099987825">"Επιλέχθηκαν %1$d"</item> + <item quantity="other" msgid="53105607141906130">"Επιλέχθηκαν %1$d"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"Επιλέχθηκαν %1$d"</item> + <item quantity="one" msgid="5030162638216034260">"Επιλέχθηκαν %1$d"</item> + <item quantity="other" msgid="3512041363942842738">"Επιλέχθηκαν %1$d"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"Εμφάνιση στον χάρτη"</string> + <string name="rotate_left" msgid="7412075232752726934">"Αριστερή περιστροφή"</string> + <string name="rotate_right" msgid="7340681085011826618">"Δεξιά περιστροφή"</string> + <string name="no_such_item" msgid="3161074758669642065">"Το αντικείμενο δεν βρέθηκε"</string> + <string name="edit" msgid="1502273844748580847">"Επεξεργασία"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Δεν υπάρχει διαθέσιμη εφαρμογή"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Επεξεργασία αιτημάτων προσωρινής αποθήκευσης"</string> + <string name="caching_label" msgid="3244800874547101776">"Προσωρ. αποθ..."</string> + <string name="crop" msgid="7970750655414797277">"Περικοπή"</string> + <string name="set_as" msgid="3636764710790507868">"Ορισμός ως"</string> + <string name="video_err" msgid="7917736494827857757">"Δεν είναι δυνατή η αναπαραγωγή του βίντεο"</string> + <string name="group_by_location" msgid="316641628989023253">"Κατά τοποθεσία"</string> + <string name="group_by_time" msgid="9046168567717963573">"Κατά ώρα"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Κατά ετικέτα"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Κατά άτομα"</string> + <string name="group_by_album" msgid="1532818636053818958">"Κατά λεύκωμα"</string> + <string name="group_by_size" msgid="153766174950394155">"Κατά μέγεθος"</string> + <string name="untagged" msgid="7281481064509590402">"Χωρίς ετικέτα"</string> + <string name="no_location" msgid="2036710947563713111">"Δεν εμφανίζεται τοποθεσία"</string> + <string name="show_images_only" msgid="7263218480867672653">"Μόνο εικόνες"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Μόνο βίντεο"</string> + <string name="show_all" msgid="4780647751652596980">"Εικόνες και βίντεο"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Συλλογή φωτογραφιών"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Δεν υπάρχουν φωτογραφίες"</string> + <string name="crop_saved" msgid="4684933379430649946">"Η εικόνα αποκοπής αποθηκεύτηκε κατά τη λήψη"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Η εικόνα αποκοπής δεν έχει αποθηκευτεί"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Δεν υπάρχουν διαθέσιμα λευκώματα"</string> + <string name="empty_album" msgid="6307897398825514762">"Δεν υπάρχουν διαθέσιμες εικόνες/βίντεο"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Λευκώματα Ιστού Picasa"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Διαθέσιμα εκτός σύνδεσης"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Τέλος"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d από %2$d στοιχεία:"</string> + <string name="title" msgid="7622928349908052569">"Τίτλος"</string> + <string name="description" msgid="3016729318096557520">"Περιγραφή"</string> + <string name="time" msgid="1367953006052876956">"Ώρα"</string> + <string name="location" msgid="3432705876921618314">"Τοποθεσία"</string> + <string name="path" msgid="4725740395885105824">"Διαδρομή"</string> + <string name="width" msgid="9215847239714321097">"Πλάτος"</string> + <string name="height" msgid="3648885449443787772">"Ύψος"</string> + <string name="orientation" msgid="4958327983165245513">"Προσανατολισμός"</string> + <string name="duration" msgid="8160058911218541616">"Διάρκεια"</string> + <string name="mimetype" msgid="3518268469266183548">"Τύπος MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Μέγ. αρχείου"</string> + <string name="maker" msgid="7921835498034236197">"Δημιουργός"</string> + <string name="model" msgid="8240207064064337366">"Μοντέλο"</string> + <string name="flash" msgid="2816779031261147723">"Φλας"</string> + <string name="aperture" msgid="5920657630303915195">"Διάφραγμα"</string> + <string name="focal_length" msgid="1291383769749877010">"Μήκος εστίασης"</string> + <string name="white_balance" msgid="8122534414851280901">"Ισορροπία λευκού"</string> + <string name="exposure_time" msgid="3146642210127439553">"Χρόνος έκθεσης"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"χιλιοστά"</string> + <string name="manual" msgid="6608905477477607865">"Μη αυτόματο"</string> + <string name="auto" msgid="4296941368722892821">"Αυτόματο"</string> + <string name="flash_on" msgid="7891556231891837284">"Το φλας άναψε"</string> + <string name="flash_off" msgid="1445443413822680010">"Όχι φλας"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Διάθεση λευκώματος εκτός σύνδεσης"</item> + <item quantity="other" msgid="6929905722448632886">"Διάθεση λευκωμάτων εκτός σύνδεσης"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Αυτό το αντικείμενο είναι αποθηκευμένο τοπικά και διαθέσιμο εκτός σύνδεσης."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Όλα τα Λευκώματα"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Τοπικά Λευκώματα"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"Συσκευές MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Λευκώματα Picasa"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> ελεύθερα"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> ή μικρότερο"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> ή μεγαλύτερο"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> έως <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Εισαγωγή"</string> + <string name="import_complete" msgid="1098450310074640619">"Εισαγ. ολοκληρώθηκε"</string> + <string name="import_fail" msgid="5205927625132482529">"Αποτυχία εισαγωγής"</string> + <string name="camera_connected" msgid="6984353643349303075">"Φωτογραφική μηχανή συνδεδεμένη"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Φωτογρ. μηχανή αποσυνδεδεμένη"</string> + <string name="click_import" msgid="6407959065464291972">"Αγγίξτε εδώ για να κάνετε εισαγωγή"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Εικόνες από ένα λεύκωμα"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Τυχαία αναπ. όλων των εικόνων"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Επιλέξτε μια εικόνα"</string> + <string name="widget_type" msgid="7308564524449340985">"Τύπος γραφ. στοιχ."</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Προβολή διαφανειών"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Προαναζήτηση φωτογραφιών Picasa:"</string> + <string name="cache_status" msgid="7690438435538533106">"Λήψη <xliff:g id="NUMBER_0">%1$s</xliff:g> από <xliff:g id="NUMBER_1">%2$s</xliff:g> φωτογρ."</string> + <string name="cache_done" msgid="9194449192869777483">"Ολοκλήρωση λήψης"</string> + <string name="albums" msgid="7320787705180057947">"Άλμπουμ"</string> + <string name="times" msgid="2023033894889499219">"Φορές"</string> + <string name="locations" msgid="6649297994083130305">"Τοποθεσίες"</string> + <string name="people" msgid="4114003823747292747">"Άτομα"</string> + <string name="tags" msgid="5539648765482935955">"Ετικέτες"</string> + <string name="group_by" msgid="4308299657902209357">"Ομαδοποίηση κατά"</string> + <string name="settings" msgid="1534847740615665736">"Ρυθμίσεις"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"Ρυθμίσεις λογαριασμού"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Ρυθμίσεις χρήσης δεδομένων"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Αυτόματη μεταφόρτωση"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Άλλες ρυθμίσεις"</string> + <string name="about_gallery" msgid="8667445445883757255">"Σχετικά με το Gallery"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Συγχρονισμός μόνο σε WiFi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Αυτόματη μεταφόρτωση όλων των ληφθέντων φωτογραφιών και βίντεο σε ιδιωτικό λεύκωμα ιστού picasa"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Ενεργοποίηση αυτόματης μεταφόρτωσης"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Συγχρ.φωτογρ.Google ΕΝΕΡΓΟΣ"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Συγχρ.φωτογρ.Google ΑΝΕΝΕΡΓΟΣ"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Αλλάξτε τις προτι.συγχρ.ή καταρ.λογαρ."</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Προβολή φωτογραφιών και βίντεο από λογαριασμό στο Gallery"</string> + <string name="add_account" msgid="4271217504968243974">"Προσθήκη λογαριασμού"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Λογαρ. για αυτόμ. μεταφόρτωση"</string> +</resources> diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml new file mode 100644 index 000000000..24dd65370 --- /dev/null +++ b/res/values-en-rGB/strings.xml @@ -0,0 +1,177 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Gallery"</string> + <string name="gadget_title" msgid="259405922673466798">"Picture frame"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Video player"</string> + <string name="loading_video" msgid="4013492720121891585">"Loading video…"</string> + <string name="loading_image" msgid="1200894415793838191">"Loading image…"</string> + <string name="loading_account" msgid="928195413034552034">"Loading account???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Resume video"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Resume playing from %s ?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Resume playing"</string> + <string name="loading" msgid="7038208555304563571">"Loading…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"Failed to load"</string> + <string name="no_thumbnail" msgid="284723185546429750">"No thumbnail"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Start again"</string> + <string name="crop_save_text" msgid="8821167985419282305">"OK"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Tap a face to begin."</string> + <string name="saving_image" msgid="7270334453636349407">"Saving picture…"</string> + <string name="crop_label" msgid="521114301871349328">"Crop picture"</string> + <string name="select_image" msgid="7841406150484742140">"Select photo"</string> + <string name="select_video" msgid="4859510992798615076">"Select video"</string> + <string name="select_item" msgid="2257529413100472599">"Select item(s)"</string> + <string name="select_album" msgid="4632641262236697235">"Select album(s)"</string> + <string name="select_group" msgid="9090385962030340391">"Select group(s)"</string> + <string name="set_image" msgid="2331476809308010401">"Set picture as"</string> + <string name="wallpaper" msgid="9222901738515471972">"Setting wallpaper, please wait…"</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Wallpaper"</string> + <string name="delete" msgid="2839695998251824487">"Delete"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Confirm Deletion"</string> + <string name="cancel" msgid="3637516880917356226">"Cancel"</string> + <string name="share" msgid="3619042788254195341">"Share"</string> + <string name="select_all" msgid="8623593677101437957">"Select All"</string> + <string name="deselect_all" msgid="7397531298370285581">"Deselect All"</string> + <string name="slideshow" msgid="4355906903247112975">"Slideshow"</string> + <string name="details" msgid="8415120088556445230">"Details"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Switch to camera"</string> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"%1$d selected"</item> + <item quantity="one" msgid="2478365152745637768">"%1$d selected"</item> + <item quantity="other" msgid="754722656147810487">"%1$d selected"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"%1$d selected"</item> + <item quantity="one" msgid="6184377003099987825">"%1$d selected"</item> + <item quantity="other" msgid="53105607141906130">"%1$d selected"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"%1$d selected"</item> + <item quantity="one" msgid="5030162638216034260">"%1$d selected"</item> + <item quantity="other" msgid="3512041363942842738">"%1$d selected"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"Show on map"</string> + <string name="rotate_left" msgid="7412075232752726934">"Rotate left"</string> + <string name="rotate_right" msgid="7340681085011826618">"Rotate right"</string> + <string name="no_such_item" msgid="3161074758669642065">"Item not found"</string> + <string name="edit" msgid="1502273844748580847">"Edit"</string> + <string name="activity_not_found" msgid="3731390759313019518">"No application available"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Process Caching Requests"</string> + <string name="caching_label" msgid="3244800874547101776">"Caching..."</string> + <string name="crop" msgid="7970750655414797277">"Crop"</string> + <string name="set_as" msgid="3636764710790507868">"Set as"</string> + <string name="video_err" msgid="7917736494827857757">"Unable to play video"</string> + <string name="group_by_location" msgid="316641628989023253">"By location"</string> + <string name="group_by_time" msgid="9046168567717963573">"By time"</string> + <string name="group_by_tags" msgid="3568731317210676160">"By tags"</string> + <string name="group_by_faces" msgid="1566351636227274906">"By people"</string> + <string name="group_by_album" msgid="1532818636053818958">"By album"</string> + <string name="group_by_size" msgid="153766174950394155">"By size"</string> + <string name="untagged" msgid="7281481064509590402">"Untagged"</string> + <string name="no_location" msgid="2036710947563713111">"No Location"</string> + <string name="show_images_only" msgid="7263218480867672653">"Images only"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Videos only"</string> + <string name="show_all" msgid="4780647751652596980">"Images and videos"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Photo Gallery"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"No photos."</string> + <string name="crop_saved" msgid="4684933379430649946">"The cropped image has been saved in download"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"The cropped image has not been saved"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"There are no albums available"</string> + <string name="empty_album" msgid="6307897398825514762">"There are no images/videos available"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa Web Albums"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Make available offline"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Done"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d of %2$d items:"</string> + <string name="title" msgid="7622928349908052569">"Title"</string> + <string name="description" msgid="3016729318096557520">"Description"</string> + <string name="time" msgid="1367953006052876956">"Time"</string> + <string name="location" msgid="3432705876921618314">"Location"</string> + <string name="path" msgid="4725740395885105824">"Path"</string> + <string name="width" msgid="9215847239714321097">"Width"</string> + <string name="height" msgid="3648885449443787772">"Height"</string> + <string name="orientation" msgid="4958327983165245513">"Orientation"</string> + <string name="duration" msgid="8160058911218541616">"Duration"</string> + <string name="mimetype" msgid="3518268469266183548">"MIME Type"</string> + <string name="file_size" msgid="4670384449129762138">"File Size"</string> + <string name="maker" msgid="7921835498034236197">"Maker"</string> + <string name="model" msgid="8240207064064337366">"Model"</string> + <string name="flash" msgid="2816779031261147723">"Flash"</string> + <string name="aperture" msgid="5920657630303915195">"Aperture"</string> + <string name="focal_length" msgid="1291383769749877010">"Focal Length"</string> + <string name="white_balance" msgid="8122534414851280901">"White Balance"</string> + <string name="exposure_time" msgid="3146642210127439553">"Exposure Time"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Manual"</string> + <string name="auto" msgid="4296941368722892821">"Auto"</string> + <string name="flash_on" msgid="7891556231891837284">"Flash fired"</string> + <string name="flash_off" msgid="1445443413822680010">"No flash"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Making album available offline"</item> + <item quantity="other" msgid="6929905722448632886">"Making albums available offline"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"This item is stored locally and available offline."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"All Albums"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Local Albums"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"MTP Devices"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Picasa Albums"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> free"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> or below"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> or above"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> to <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Import"</string> + <string name="import_complete" msgid="1098450310074640619">"Import Complete"</string> + <string name="import_fail" msgid="5205927625132482529">"Import Fail"</string> + <string name="camera_connected" msgid="6984353643349303075">"Camera connected"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Camera disconnected"</string> + <string name="click_import" msgid="6407959065464291972">"Touch here to import"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Images from an album"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Shuffle all images"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Pick an image"</string> + <string name="widget_type" msgid="7308564524449340985">"Widget Type"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Slide show"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Prefetching Picasa photos:"</string> + <string name="cache_status" msgid="7690438435538533106">"Download <xliff:g id="NUMBER_0">%1$s</xliff:g> of <xliff:g id="NUMBER_1">%2$s</xliff:g> photos"</string> + <string name="cache_done" msgid="9194449192869777483">"Download complete"</string> + <string name="albums" msgid="7320787705180057947">"Albums"</string> + <string name="times" msgid="2023033894889499219">"Times"</string> + <string name="locations" msgid="6649297994083130305">"Locations"</string> + <string name="people" msgid="4114003823747292747">"People"</string> + <string name="tags" msgid="5539648765482935955">"Tags"</string> + <string name="group_by" msgid="4308299657902209357">"Group by"</string> + <string name="settings" msgid="1534847740615665736">"Settings"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"Account settings"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Data usage settings"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Auto-upload"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Other settings"</string> + <string name="about_gallery" msgid="8667445445883757255">"About Gallery"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Sync on Wi-Fi only"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Automatically upload all the photos and videos that you take to a private Picasa web album"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Enable Auto-upload"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Google photo sync is ON"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Google photo sync is OFF"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Change sync preferences or remove this account"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"View photos and videos from this account in the Gallery"</string> + <string name="add_account" msgid="4271217504968243974">"Add account"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Choose Auto-upload account"</string> +</resources> diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml new file mode 100644 index 000000000..455d5d071 --- /dev/null +++ b/res/values-es-rUS/strings.xml @@ -0,0 +1,177 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galería"</string> + <string name="gadget_title" msgid="259405922673466798">"Marco de imagen"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Reproductor de video."</string> + <string name="loading_video" msgid="4013492720121891585">"Cargando el video..."</string> + <string name="loading_image" msgid="1200894415793838191">"Cargando imagen..."</string> + <string name="loading_account" msgid="928195413034552034">"Cargando cuenta..."</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Retomar video"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"¿Deseas retomar la reproducción desde %s?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Retomar la reproducción"</string> + <string name="loading" msgid="7038208555304563571">"Cargando…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"Se produjo un error al realizar la carga."</string> + <string name="no_thumbnail" msgid="284723185546429750">"Sin miniatura"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Empezar de nuevo"</string> + <string name="crop_save_text" msgid="8821167985419282305">"Aceptar"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Toca una cara para empezar."</string> + <string name="saving_image" msgid="7270334453636349407">"Guardando imagen..."</string> + <string name="crop_label" msgid="521114301871349328">"Recortar imagen"</string> + <string name="select_image" msgid="7841406150484742140">"Seleccionar foto"</string> + <string name="select_video" msgid="4859510992798615076">"Seleccionar video"</string> + <string name="select_item" msgid="2257529413100472599">"Seleccionar artículo(s)"</string> + <string name="select_album" msgid="4632641262236697235">"Seleccionar álbum(es)"</string> + <string name="select_group" msgid="9090385962030340391">"Seleccionar grupo(s)"</string> + <string name="set_image" msgid="2331476809308010401">"Establecer imagen como"</string> + <string name="wallpaper" msgid="9222901738515471972">"Configurando papel tapiz. Espera, por favor..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Papel tapiz"</string> + <string name="delete" msgid="2839695998251824487">"Eliminar"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Confirmar eliminación"</string> + <string name="cancel" msgid="3637516880917356226">"Cancelar"</string> + <string name="share" msgid="3619042788254195341">"Compartir"</string> + <string name="select_all" msgid="8623593677101437957">"Seleccionar todo"</string> + <string name="deselect_all" msgid="7397531298370285581">"Desmarcar todos"</string> + <string name="slideshow" msgid="4355906903247112975">"Presentación de diapositivas"</string> + <string name="details" msgid="8415120088556445230">"Detalles"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Cambiar a cámara"</string> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"%1$d seleccionado(s)"</item> + <item quantity="one" msgid="2478365152745637768">"%1$d seleccionado(s)"</item> + <item quantity="other" msgid="754722656147810487">"%1$d seleccionado(s)"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"%1$d seleccionado(s)"</item> + <item quantity="one" msgid="6184377003099987825">"%1$d seleccionado(s)"</item> + <item quantity="other" msgid="53105607141906130">"%1$d seleccionado(s)"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"%1$d seleccionado(s)"</item> + <item quantity="one" msgid="5030162638216034260">"%1$d seleccionado(s)"</item> + <item quantity="other" msgid="3512041363942842738">"%1$d seleccionado(s)"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"Mostrar en el mapa"</string> + <string name="rotate_left" msgid="7412075232752726934">"Rotar hacia la izquierda"</string> + <string name="rotate_right" msgid="7340681085011826618">"Rotar hacia la derecha"</string> + <string name="no_such_item" msgid="3161074758669642065">"No se encontró el elemento"</string> + <string name="edit" msgid="1502273844748580847">"Editar"</string> + <string name="activity_not_found" msgid="3731390759313019518">"No hay ninguna aplicación disponible"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Procesar solicitudes de almacenamiento en memoria caché"</string> + <string name="caching_label" msgid="3244800874547101776">"Alm. en caché..."</string> + <string name="crop" msgid="7970750655414797277">"Recortar"</string> + <string name="set_as" msgid="3636764710790507868">"Establecer como"</string> + <string name="video_err" msgid="7917736494827857757">"No se puede reproducir el video."</string> + <string name="group_by_location" msgid="316641628989023253">"Por ubicación"</string> + <string name="group_by_time" msgid="9046168567717963573">"Por fecha"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Por etiquetas"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Por persona"</string> + <string name="group_by_album" msgid="1532818636053818958">"Por álbum"</string> + <string name="group_by_size" msgid="153766174950394155">"Por tamaño"</string> + <string name="untagged" msgid="7281481064509590402">"No etiquetado"</string> + <string name="no_location" msgid="2036710947563713111">"No hay ubicación"</string> + <string name="show_images_only" msgid="7263218480867672653">"Sólo imágenes"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Sólo videos"</string> + <string name="show_all" msgid="4780647751652596980">"Imágenes y videos"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Galería de fotos"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"No hay fotos."</string> + <string name="crop_saved" msgid="4684933379430649946">"La imagen recortada se guardó en descarga."</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Imagen recortada no se guardó."</string> + <string name="no_albums_alert" msgid="3459550423604532470">"No hay álbumes disponibles"</string> + <string name="empty_album" msgid="6307897398825514762">"No hay imágenes/videos disponibles"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Álbumes web de Picasa"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Hacer disponible sin conexión"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Listo"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d de %2$d elementos:"</string> + <string name="title" msgid="7622928349908052569">"Título"</string> + <string name="description" msgid="3016729318096557520">"Descripción"</string> + <string name="time" msgid="1367953006052876956">"Hora"</string> + <string name="location" msgid="3432705876921618314">"Ubicación"</string> + <string name="path" msgid="4725740395885105824">"Ruta"</string> + <string name="width" msgid="9215847239714321097">"Ancho"</string> + <string name="height" msgid="3648885449443787772">"Altura"</string> + <string name="orientation" msgid="4958327983165245513">"Orientación"</string> + <string name="duration" msgid="8160058911218541616">"Duración"</string> + <string name="mimetype" msgid="3518268469266183548">"Tipo MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Tamaño de archivo"</string> + <string name="maker" msgid="7921835498034236197">"Creador"</string> + <string name="model" msgid="8240207064064337366">"Modelo"</string> + <string name="flash" msgid="2816779031261147723">"Flash"</string> + <string name="aperture" msgid="5920657630303915195">"Apertura"</string> + <string name="focal_length" msgid="1291383769749877010">"Longitud focal"</string> + <string name="white_balance" msgid="8122534414851280901">"Equilibrio de blancos"</string> + <string name="exposure_time" msgid="3146642210127439553">"Tiempo exposic"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Manual"</string> + <string name="auto" msgid="4296941368722892821">"Automático"</string> + <string name="flash_on" msgid="7891556231891837284">"Flash activado"</string> + <string name="flash_off" msgid="1445443413822680010">"Sin flash"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Permitiendo que los álbumes estén disponibles sin conexión"</item> + <item quantity="other" msgid="6929905722448632886">"Permitiendo que los álbumes estén disponibles sin conexión"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Este elemento se guardó de manera local y se encuentra disponible sin conexión."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Todos los álbumes"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Álbumes locales"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"Dispositivos MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Álbumes de Picasa"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> libre"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> o menos"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> o más"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> a <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Importar"</string> + <string name="import_complete" msgid="1098450310074640619">"Importación completa"</string> + <string name="import_fail" msgid="5205927625132482529">"Se produjo un error en la importación."</string> + <string name="camera_connected" msgid="6984353643349303075">"Se conectó la cámara."</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Se desconectó la cámara."</string> + <string name="click_import" msgid="6407959065464291972">"Toca aquí para importar."</string> + <string name="widget_type_album" msgid="3245149644830731121">"Imágenes de un álbum"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Reproducir todas las imágenes aleatoriamente"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Elegir una imagen"</string> + <string name="widget_type" msgid="7308564524449340985">"Tipo de widget"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Presentación de diapositivas"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Obtención previa de fotos de Picasa:"</string> + <string name="cache_status" msgid="7690438435538533106">"Descargar <xliff:g id="NUMBER_0">%1$s</xliff:g> de <xliff:g id="NUMBER_1">%2$s</xliff:g> fotos"</string> + <string name="cache_done" msgid="9194449192869777483">"Descarga completa"</string> + <string name="albums" msgid="7320787705180057947">"Álbumes"</string> + <string name="times" msgid="2023033894889499219">"Horarios"</string> + <string name="locations" msgid="6649297994083130305">"Ubicaciones"</string> + <string name="people" msgid="4114003823747292747">"Personas"</string> + <string name="tags" msgid="5539648765482935955">"Etiquetas"</string> + <string name="group_by" msgid="4308299657902209357">"Agrupar por"</string> + <string name="settings" msgid="1534847740615665736">"Configuración"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"Configuración de la cuenta"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Configuración de uso de datos"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Carga automática"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Otros parámetros de configuración"</string> + <string name="about_gallery" msgid="8667445445883757255">"Acerca de la Galería"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Sincronización solo en WiFi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Cargar automáticamente todas tus fotos y videos en un álbum web de Picasa privado"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Habilitar carga automática"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Sinc de Google Fotos ACTIVADA"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Sinc de Google Fotos DESACTIV"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Cambiar pref de sinc o eliminar cuenta"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Ver fotos y videos de esta cuenta en la Galería"</string> + <string name="add_account" msgid="4271217504968243974">"Agregar cuenta"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Elegir cuenta de carga automát"</string> +</resources> diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml new file mode 100644 index 000000000..97b7fa655 --- /dev/null +++ b/res/values-es/strings.xml @@ -0,0 +1,177 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galería"</string> + <string name="gadget_title" msgid="259405922673466798">"Picture frame"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Reproductor de vídeo"</string> + <string name="loading_video" msgid="4013492720121891585">"Cargando vídeo…"</string> + <string name="loading_image" msgid="1200894415793838191">"Cargando imagen…"</string> + <string name="loading_account" msgid="928195413034552034">"Cargando cuenta..."</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Reanudar vídeo"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Reanudar reproducción a partir de %s ?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Reanudar reproducción"</string> + <string name="loading" msgid="7038208555304563571">"Cargando..."</string> + <string name="fail_to_load" msgid="2710120770735315683">"Error al cargar"</string> + <string name="no_thumbnail" msgid="284723185546429750">"No hay miniaturas."</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Volver a reproducir"</string> + <string name="crop_save_text" msgid="8821167985419282305">"Aceptar"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Toca una cara para empezar."</string> + <string name="saving_image" msgid="7270334453636349407">"Guardando imagen..."</string> + <string name="crop_label" msgid="521114301871349328">"Recortar imagen"</string> + <string name="select_image" msgid="7841406150484742140">"Seleccionar foto"</string> + <string name="select_video" msgid="4859510992798615076">"Seleccionar vídeo"</string> + <string name="select_item" msgid="2257529413100472599">"Seleccionar elementos"</string> + <string name="select_album" msgid="4632641262236697235">"Seleccionar álbumes"</string> + <string name="select_group" msgid="9090385962030340391">"Seleccionar grupos"</string> + <string name="set_image" msgid="2331476809308010401">"Establecer imagen como"</string> + <string name="wallpaper" msgid="9222901738515471972">"Estableciendo fondo de pantalla..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Fondo de pantalla"</string> + <string name="delete" msgid="2839695998251824487">"Borrar"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Confirmar eliminación"</string> + <string name="cancel" msgid="3637516880917356226">"Cancelar"</string> + <string name="share" msgid="3619042788254195341">"Compartir"</string> + <string name="select_all" msgid="8623593677101437957">"Seleccionar todo"</string> + <string name="deselect_all" msgid="7397531298370285581">"Desmarcar todo"</string> + <string name="slideshow" msgid="4355906903247112975">"Presentación"</string> + <string name="details" msgid="8415120088556445230">"Detalles"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Cambiar a la cámara"</string> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"%1$d seleccionados"</item> + <item quantity="one" msgid="2478365152745637768">"%1$d seleccionados"</item> + <item quantity="other" msgid="754722656147810487">"%1$d seleccionados"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"%1$d seleccionados"</item> + <item quantity="one" msgid="6184377003099987825">"%1$d seleccionados"</item> + <item quantity="other" msgid="53105607141906130">"%1$d seleccionados"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"%1$d seleccionados"</item> + <item quantity="one" msgid="5030162638216034260">"%1$d seleccionados"</item> + <item quantity="other" msgid="3512041363942842738">"%1$d seleccionados"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"Mostrar en el mapa"</string> + <string name="rotate_left" msgid="7412075232752726934">"Girar a la izquierda"</string> + <string name="rotate_right" msgid="7340681085011826618">"Girar a la derecha"</string> + <string name="no_such_item" msgid="3161074758669642065">"No se ha encontrado el elemento."</string> + <string name="edit" msgid="1502273844748580847">"Editar"</string> + <string name="activity_not_found" msgid="3731390759313019518">"No hay ninguna aplicación disponible."</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Procesar solicitudes de almacenamiento en caché"</string> + <string name="caching_label" msgid="3244800874547101776">"Alm en caché..."</string> + <string name="crop" msgid="7970750655414797277">"Recortar"</string> + <string name="set_as" msgid="3636764710790507868">"Establecer como"</string> + <string name="video_err" msgid="7917736494827857757">"No se puede reproducir ningún vídeo."</string> + <string name="group_by_location" msgid="316641628989023253">"Por ubicación"</string> + <string name="group_by_time" msgid="9046168567717963573">"Por fecha"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Por etiquetas"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Por personas"</string> + <string name="group_by_album" msgid="1532818636053818958">"Por álbum"</string> + <string name="group_by_size" msgid="153766174950394155">"Por tamaño"</string> + <string name="untagged" msgid="7281481064509590402">"Sin etiquetas"</string> + <string name="no_location" msgid="2036710947563713111">"Sin ubicación"</string> + <string name="show_images_only" msgid="7263218480867672653">"Sólo imágenes"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Sólo vídeos"</string> + <string name="show_all" msgid="4780647751652596980">"Imágenes y vídeos"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Galería de fotos"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"No hay fotos."</string> + <string name="crop_saved" msgid="4684933379430649946">"Imagen recortada guardada en carpeta de descargas"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Imagen recortada no guardada"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"No hay álbumes disponibles."</string> + <string name="empty_album" msgid="6307897398825514762">"No hay imágenes ni vídeos disponibles."</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Álbumes web de Picasa"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Google Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Disponible sin conexión"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Listo"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d de %2$d elementos:"</string> + <string name="title" msgid="7622928349908052569">"Título"</string> + <string name="description" msgid="3016729318096557520">"Descripción"</string> + <string name="time" msgid="1367953006052876956">"Hora"</string> + <string name="location" msgid="3432705876921618314">"Ubicación"</string> + <string name="path" msgid="4725740395885105824">"Ruta"</string> + <string name="width" msgid="9215847239714321097">"Ancho"</string> + <string name="height" msgid="3648885449443787772">"Altura"</string> + <string name="orientation" msgid="4958327983165245513">"Orientación"</string> + <string name="duration" msgid="8160058911218541616">"Duración"</string> + <string name="mimetype" msgid="3518268469266183548">"Tipo MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Tamaño archivo"</string> + <string name="maker" msgid="7921835498034236197">"Creador"</string> + <string name="model" msgid="8240207064064337366">"Modelo"</string> + <string name="flash" msgid="2816779031261147723">"Flash"</string> + <string name="aperture" msgid="5920657630303915195">"Apertura"</string> + <string name="focal_length" msgid="1291383769749877010">"Long enfoque"</string> + <string name="white_balance" msgid="8122534414851280901">"Balance blancos"</string> + <string name="exposure_time" msgid="3146642210127439553">"Tiempo exposic"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Manual"</string> + <string name="auto" msgid="4296941368722892821">"Automát"</string> + <string name="flash_on" msgid="7891556231891837284">"Flash activado"</string> + <string name="flash_off" msgid="1445443413822680010">"Sin flash"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Haciendo que el álbum pueda verse sin conexión"</item> + <item quantity="other" msgid="6929905722448632886">"Haciendo que los álbumes puedan verse sin conexión"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"El elemento se ha almacenado de forma local y está disponible sin conexión."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Todos los álbumes"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Álbumes locales"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"Dispositivos MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Álbumes de Picasa"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> libres"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> o inferior"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> o superior"</string> + <string name="size_between" msgid="8779660840898917208">"De <xliff:g id="MIN_SIZE">%1$s</xliff:g> a <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Importar"</string> + <string name="import_complete" msgid="1098450310074640619">"Importación completada"</string> + <string name="import_fail" msgid="5205927625132482529">"Error al importar"</string> + <string name="camera_connected" msgid="6984353643349303075">"Cámara conectada"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Cámara desconectada"</string> + <string name="click_import" msgid="6407959065464291972">"Toca aquí para realizar la importación."</string> + <string name="widget_type_album" msgid="3245149644830731121">"Imágenes de un álbum"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Mostrar imágenes aleatoriamente"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Seleccionar una imagen"</string> + <string name="widget_type" msgid="7308564524449340985">"Tipo de widget"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Presentación"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Recopilando fotos de Picasa:"</string> + <string name="cache_status" msgid="7690438435538533106">"Descarga: <xliff:g id="NUMBER_0">%1$s</xliff:g> de <xliff:g id="NUMBER_1">%2$s</xliff:g> fotos"</string> + <string name="cache_done" msgid="9194449192869777483">"Descarga completada"</string> + <string name="albums" msgid="7320787705180057947">"Álbumes"</string> + <string name="times" msgid="2023033894889499219">"Horas"</string> + <string name="locations" msgid="6649297994083130305">"Ubicaciones"</string> + <string name="people" msgid="4114003823747292747">"Personas"</string> + <string name="tags" msgid="5539648765482935955">"Etiquetas"</string> + <string name="group_by" msgid="4308299657902209357">"Agrupar por"</string> + <string name="settings" msgid="1534847740615665736">"Ajustes"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"Ajustes de la cuenta"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Ajustes de uso de datos"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Subida automática"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Otros ajustes"</string> + <string name="about_gallery" msgid="8667445445883757255">"Acerca de la galería"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Sincronización solo en Wi-Fi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Subir automáticamente todas las fotos y los vídeos realizados a un álbum web privado de Picasa"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Habilitar subida automática"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Sincronización fotos activada"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Sincronización fotos desactivada"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Cambiar pref sincr o eliminar cuenta"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Ver fotos y vídeos de esta cuenta en la galería"</string> + <string name="add_account" msgid="4271217504968243974">"Añadir cuenta"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Seleccionar cuenta subida auto"</string> +</resources> diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml new file mode 100644 index 000000000..033c34fa3 --- /dev/null +++ b/res/values-fa/strings.xml @@ -0,0 +1,173 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"گالری"</string> + <string name="gadget_title" msgid="259405922673466798">"قاب عکس"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"پخش کننده ویدیو"</string> + <string name="loading_video" msgid="4013492720121891585">"در حال بارگیری ویدیو..."</string> + <string name="loading_image" msgid="1200894415793838191">"در حال بارگیری تصویر …"</string> + <string name="loading_account" msgid="928195413034552034">"بارگیری حساب؟؟؟"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"از سرگیری ویدیو"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"ادامه پخش از %s ؟"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"از سرگیری پخش"</string> + <string name="loading" msgid="7038208555304563571">"در حال بارگیری…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"بارگیری نشد"</string> + <string name="no_thumbnail" msgid="284723185546429750">"تصویر کوچکی وجود ندارد"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"شروع مجدد"</string> + <string name="crop_save_text" msgid="8821167985419282305">"تأیید"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"چهره ای را برای شروع ضربه بزنید."</string> + <string name="saving_image" msgid="7270334453636349407">"در حال ذخیره عکس..."</string> + <string name="crop_label" msgid="521114301871349328">"برش تصویر"</string> + <string name="select_image" msgid="7841406150484742140">"انتخاب عکس"</string> + <string name="select_video" msgid="4859510992798615076">"انتخاب ویدیو"</string> + <string name="select_item" msgid="2257529413100472599">"انتخاب مورد(موارد)"</string> + <string name="select_album" msgid="4632641262236697235">"انتخاب آلبوم(ها)"</string> + <string name="select_group" msgid="9090385962030340391">"انتخاب گروه(ها)"</string> + <string name="set_image" msgid="2331476809308010401">"تنظیم تصویر بعنوان"</string> + <string name="wallpaper" msgid="9222901738515471972">"تنظیم تصویر زمینه، لطفاً منتظر بمانید..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"تصویر زمینه"</string> + <string name="delete" msgid="2839695998251824487">"حذف"</string> + <string name="confirm_delete" msgid="5731757674837098707">"تأیید حذف"</string> + <string name="cancel" msgid="3637516880917356226">"لغو"</string> + <string name="share" msgid="3619042788254195341">"اشتراک گذاری"</string> + <string name="select_all" msgid="8623593677101437957">"انتخاب همه"</string> + <string name="deselect_all" msgid="7397531298370285581">"لغو انتخاب همه"</string> + <string name="slideshow" msgid="4355906903247112975">"نمایش اسلاید"</string> + <string name="details" msgid="8415120088556445230">"جزئیات"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"جابجایی به دوربین"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"نمایش در نقشه"</string> + <string name="rotate_left" msgid="7412075232752726934">"چرخش به چپ"</string> + <string name="rotate_right" msgid="7340681085011826618">"چرخش به راست"</string> + <string name="no_such_item" msgid="3161074758669642065">"مورد یافت نشد"</string> + <string name="edit" msgid="1502273844748580847">"ویرایش"</string> + <string name="activity_not_found" msgid="3731390759313019518">"برنامه ای در دسترس نیست"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"پردازش درخواست های ذخیره موقت"</string> + <string name="caching_label" msgid="3244800874547101776">"در حال ذخیره در حافظه پنهان..."</string> + <string name="crop" msgid="7970750655414797277">"برش"</string> + <string name="set_as" msgid="3636764710790507868">"تنظیم بعنوان"</string> + <string name="video_err" msgid="7917736494827857757">"پخش ویدیو امکان پذیر نیست"</string> + <string name="group_by_location" msgid="316641628989023253">"بر اساس محل"</string> + <string name="group_by_time" msgid="9046168567717963573">"بر اساس زمان"</string> + <string name="group_by_tags" msgid="3568731317210676160">"بر اساس برچسب ها"</string> + <string name="group_by_faces" msgid="1566351636227274906">"براساس افراد"</string> + <string name="group_by_album" msgid="1532818636053818958">"بر اساس آلبوم"</string> + <string name="group_by_size" msgid="153766174950394155">"بر اساس اندازه"</string> + <string name="untagged" msgid="7281481064509590402">"بدون برچسب گذاری"</string> + <string name="no_location" msgid="2036710947563713111">"مکانی موجود نیست"</string> + <string name="show_images_only" msgid="7263218480867672653">"فقط تصاویر"</string> + <string name="show_videos_only" msgid="3850394623678871697">"فقط ویدیوها"</string> + <string name="show_all" msgid="4780647751652596980">"تصاویر و ویدیوها"</string> + <string name="appwidget_title" msgid="6410561146863700411">"گالری عکس"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"عکسی موجود نیست"</string> + <string name="crop_saved" msgid="4684933379430649946">"تصویر برش خورده در دانلود ذخیره شده است"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"تصویر برش خورده ذخیره نشده است"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"آلبومی در دسترس نیست"</string> + <string name="empty_album" msgid="6307897398825514762">"تصویر یا ویدیویی در دسترس نیست"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa Web Albums"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"در دسترس بودن در هنگام آفلاین"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"انجام شد"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d از %2$d مورد:"</string> + <string name="title" msgid="7622928349908052569">"عنوان"</string> + <string name="description" msgid="3016729318096557520">"توصیف"</string> + <string name="time" msgid="1367953006052876956">"زمان"</string> + <string name="location" msgid="3432705876921618314">"موقعیت مکانی"</string> + <string name="path" msgid="4725740395885105824">"مسیر"</string> + <string name="width" msgid="9215847239714321097">"عرض"</string> + <string name="height" msgid="3648885449443787772">"ارتفاع"</string> + <string name="orientation" msgid="4958327983165245513">"جهت"</string> + <string name="duration" msgid="8160058911218541616">"مدت"</string> + <string name="mimetype" msgid="3518268469266183548">"نوع MIME"</string> + <string name="file_size" msgid="4670384449129762138">"اندازه فایل"</string> + <string name="maker" msgid="7921835498034236197">"سازنده"</string> + <string name="model" msgid="8240207064064337366">"مدل"</string> + <string name="flash" msgid="2816779031261147723">"فلاش"</string> + <string name="aperture" msgid="5920657630303915195">"دریچه دیافراگم"</string> + <string name="focal_length" msgid="1291383769749877010">"فاصله کانونی"</string> + <string name="white_balance" msgid="8122534414851280901">"توازن سفیدی"</string> + <string name="exposure_time" msgid="3146642210127439553">"زمان نوردهی"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"میلیمتر"</string> + <string name="manual" msgid="6608905477477607865">"دستی"</string> + <string name="auto" msgid="4296941368722892821">"خودکار"</string> + <string name="flash_on" msgid="7891556231891837284">"فلاش زده شد"</string> + <string name="flash_off" msgid="1445443413822680010">"بدون فلاش"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"در دسترس قرار دادن آلبوم به صورت آفلاین"</item> + <item quantity="other" msgid="6929905722448632886">"در دسترس قرار دادن آلبوم ها به صورت آفلاین"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"این مورد به صورت محلی ذخیره می شود و به طور آفلاین در دسترس است."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"همه آلبوم ها"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"آلبوم های محلی"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"دستگاه های MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Picasa Albums"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> آزاد"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> یا کمتر"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> یا بیشتر"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> تا <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"وارد کردن"</string> + <string name="import_complete" msgid="1098450310074640619">"وارد کردن انجام شد"</string> + <string name="import_fail" msgid="5205927625132482529">"وارد کردن انجام نشد"</string> + <string name="camera_connected" msgid="6984353643349303075">"دوربین متصل شد"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"اتصال دوربین قطع شد"</string> + <string name="click_import" msgid="6407959065464291972">"برای وارد کردن اینجا را لمس کنید"</string> + <string name="widget_type_album" msgid="3245149644830731121">"عکس هایی از یک آلبوم"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"نمایش تصادفی همه تصاویر"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"یک تصویر انتخاب کنید"</string> + <string name="widget_type" msgid="7308564524449340985">"نوع ابزارک"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"نمایش اسلاید"</string> + <string name="cache_status_title" msgid="8414708919928621485">"واکشی اولیه عکس های picasa:"</string> + <string name="cache_status" msgid="7690438435538533106">"دانلود <xliff:g id="NUMBER_0">%1$s</xliff:g> از <xliff:g id="NUMBER_1">%2$s</xliff:g> عکس"</string> + <string name="cache_done" msgid="9194449192869777483">"دانلود انجام شد"</string> + <string name="albums" msgid="7320787705180057947">"آلبومها"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"مکانها"</string> + <string name="people" msgid="4114003823747292747">"افراد"</string> + <string name="tags" msgid="5539648765482935955">"نشانها"</string> + <string name="group_by" msgid="4308299657902209357">"گروه بندی براساس"</string> + <!-- no translation found for settings (1534847740615665736) --> + <skip /> + <string name="prefs_accounts" msgid="7942761992713671670">"تنظیمات حساب"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"تنظیمات استفاده از داده"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"آپلود خودکار"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"تنظیمات دیگر"</string> + <string name="about_gallery" msgid="8667445445883757255">"درباره گالری"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"همگامسازی فقط با WiFi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"به صورت خودکار تمام عکسها و ویدیوهای شما به یک آلبوم خصوصی در picasa web albums آپلود شود"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"فعال کردن آپلود خودکار"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"همگامسازی عکسهای Google روشن است"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"همگامسازی عکسهای Google خاموش است"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"تنظیمات برگزیده همگامسازی تغییر داده شود یا این حساب حذف شود"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"مشاهده عکسها و ویدیوهای این حساب در گالری"</string> + <string name="add_account" msgid="4271217504968243974">"افزودن حساب"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"حساب آپلود خودکار را انتخاب کنید"</string> +</resources> diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml new file mode 100644 index 000000000..f783ab9aa --- /dev/null +++ b/res/values-fi/strings.xml @@ -0,0 +1,172 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galleria"</string> + <string name="gadget_title" msgid="259405922673466798">"Valokuvakehys"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d.%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d.%2$02d.%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Videosoitin"</string> + <string name="loading_video" msgid="4013492720121891585">"Ladataan videota…"</string> + <string name="loading_image" msgid="1200894415793838191">"Ladataan kuvaa..."</string> + <string name="loading_account" msgid="928195413034552034">"Tiliä ladataan..."</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Jatka videon toistoa"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Jatketaanko toistoa kohdasta %s?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Jatka toistoa"</string> + <string name="loading" msgid="7038208555304563571">"Ladataan…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"Lataus epäonnistui"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Ei pikkukuvaa"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Aloita alusta"</string> + <string name="crop_save_text" msgid="8821167985419282305">"Ok"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Aloita napauttamalla kasvoja."</string> + <string name="saving_image" msgid="7270334453636349407">"Tallennetaan kuvaa…"</string> + <string name="crop_label" msgid="521114301871349328">"Leikkaa kuvaa"</string> + <string name="select_image" msgid="7841406150484742140">"Valitse valokuva"</string> + <string name="select_video" msgid="4859510992798615076">"Valitse video"</string> + <string name="select_item" msgid="2257529413100472599">"Valitse kohteet"</string> + <string name="select_album" msgid="4632641262236697235">"Valitse albumi(t)"</string> + <string name="select_group" msgid="9090385962030340391">"Valitse ryhmä(t)"</string> + <string name="set_image" msgid="2331476809308010401">"Aseta kuva"</string> + <string name="wallpaper" msgid="9222901738515471972">"Asetetaan taustakuvaa, odota…"</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"taustakuvaksi"</string> + <string name="delete" msgid="2839695998251824487">"Poista"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Vahvista poisto"</string> + <string name="cancel" msgid="3637516880917356226">"Peruuta"</string> + <string name="share" msgid="3619042788254195341">"Jaa"</string> + <string name="select_all" msgid="8623593677101437957">"Valitse kaikki"</string> + <string name="deselect_all" msgid="7397531298370285581">"Poista kaikki valinnat"</string> + <string name="slideshow" msgid="4355906903247112975">"Diaesitys"</string> + <string name="details" msgid="8415120088556445230">"Tiedot"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Vaihda kameraan"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"Näytä kartalla"</string> + <string name="rotate_left" msgid="7412075232752726934">"Kierrä vastapäivään"</string> + <string name="rotate_right" msgid="7340681085011826618">"Kierrä myötäpäivään"</string> + <string name="no_such_item" msgid="3161074758669642065">"Kohdetta ei löydy"</string> + <string name="edit" msgid="1502273844748580847">"Muokkaa"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Sovellus ei käytettävissä"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Käsittele välimuistipyynnöt"</string> + <string name="caching_label" msgid="3244800874547101776">"Vie välimuistiin"</string> + <string name="crop" msgid="7970750655414797277">"Leikkaa"</string> + <string name="set_as" msgid="3636764710790507868">"Aseta"</string> + <string name="video_err" msgid="7917736494827857757">"Videon toisto ei onnistu"</string> + <string name="group_by_location" msgid="316641628989023253">"Sijainnin mukaan"</string> + <string name="group_by_time" msgid="9046168567717963573">"Ajan mukaan"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Tunnisteiden mukaan"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Ihmiset"</string> + <string name="group_by_album" msgid="1532818636053818958">"Albumin mukaan"</string> + <string name="group_by_size" msgid="153766174950394155">"Koon mukaan"</string> + <string name="untagged" msgid="7281481064509590402">"Merkitsemättömät"</string> + <string name="no_location" msgid="2036710947563713111">"Ei sijaintia"</string> + <string name="show_images_only" msgid="7263218480867672653">"Vain kuvat"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Vain videot"</string> + <string name="show_all" msgid="4780647751652596980">"Kuvat ja videot"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Kuvagalleria"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Ei valokuvia"</string> + <string name="crop_saved" msgid="4684933379430649946">"Rajattu kuva on tallennettu latauksiin"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Rajattua kuvaa ei tallennettu"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Ei albumeita käytettävissä"</string> + <string name="empty_album" msgid="6307897398825514762">"Ei kuvia/videoita saatavilla"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa-verkkoalbumit"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Aseta offline-käytettäväksi"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Valmis"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d/%2$d kohdetta:"</string> + <string name="title" msgid="7622928349908052569">"Nimi"</string> + <string name="description" msgid="3016729318096557520">"Kuvaus"</string> + <string name="time" msgid="1367953006052876956">"Aika"</string> + <string name="location" msgid="3432705876921618314">"Sijainti"</string> + <string name="path" msgid="4725740395885105824">"Polku"</string> + <string name="width" msgid="9215847239714321097">"Leveys"</string> + <string name="height" msgid="3648885449443787772">"Korkeus"</string> + <string name="orientation" msgid="4958327983165245513">"Suunta"</string> + <string name="duration" msgid="8160058911218541616">"Kesto"</string> + <string name="mimetype" msgid="3518268469266183548">"MIME-tyyppi"</string> + <string name="file_size" msgid="4670384449129762138">"Tiedoston koko"</string> + <string name="maker" msgid="7921835498034236197">"Tekijä"</string> + <string name="model" msgid="8240207064064337366">"Malli"</string> + <string name="flash" msgid="2816779031261147723">"Salama"</string> + <string name="aperture" msgid="5920657630303915195">"Aukko"</string> + <string name="focal_length" msgid="1291383769749877010">"Polttoväli"</string> + <string name="white_balance" msgid="8122534414851280901">"Valkotasapaino"</string> + <string name="exposure_time" msgid="3146642210127439553">"Valotusaika"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Manuaalinen"</string> + <string name="auto" msgid="4296941368722892821">"Autom."</string> + <string name="flash_on" msgid="7891556231891837284">"Salama käyt."</string> + <string name="flash_off" msgid="1445443413822680010">"Ei salamaa"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Albumien asettaminen saataville offline-tilassa"</item> + <item quantity="other" msgid="6929905722448632886">"Albumien asettaminen saataville offline-tilassa"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Tämä kohde on tallennettu laitteelle ja käytettävissä offline-tilassa."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Kaikki albumit"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Paikalliset albumit"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"MTP-laitteet"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Picasa-albumit"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> vapaana"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> tai alle"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> tai yli"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> – <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Tuo"</string> + <string name="import_complete" msgid="1098450310074640619">"Tuonti valmis"</string> + <string name="import_fail" msgid="5205927625132482529">"Tuonti epäonnistui"</string> + <string name="camera_connected" msgid="6984353643349303075">"Kamera yhdistetty"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Kamera irrotettu"</string> + <string name="click_import" msgid="6407959065464291972">"Tuo koskettamalla tätä"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Albumin kuvat"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Sekoita kaikki kuvat"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Valitse kuva"</string> + <string name="widget_type" msgid="7308564524449340985">"Widgetin tyyppi"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Diaesitys"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Esihaetaan Picasa-kuvia:"</string> + <string name="cache_status" msgid="7690438435538533106">"Ladataan <xliff:g id="NUMBER_0">%1$s</xliff:g> / <xliff:g id="NUMBER_1">%2$s</xliff:g> kuvaa"</string> + <string name="cache_done" msgid="9194449192869777483">"Lataus valmis"</string> + <string name="albums" msgid="7320787705180057947">"Albumit"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"Sijainnit"</string> + <string name="people" msgid="4114003823747292747">"Henkilöt"</string> + <string name="tags" msgid="5539648765482935955">"Tunnisteet"</string> + <string name="group_by" msgid="4308299657902209357">"Ryhmittely:"</string> + <string name="settings" msgid="1534847740615665736">"Asetukset"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"Tilin asetukset"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Tietojen käyttöasetukset"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Automaattinen lähettäminen"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Muut asetukset"</string> + <string name="about_gallery" msgid="8667445445883757255">"Tietoja galleriasta"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Synkronoi vain wifi-yhteyden aikana"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Lähetä automaattisesti kaikki yksityiseen Picasa-verkkoalbumiin vietävät valokuvat ja videot"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Ota automaattinen lähettäminen käyttöön"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Kuvasynkronointi on käytössä"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Kuvasynkronointi on pois käyt."</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Vaihda synkronointiasetuksia tai poista tämä tili"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Näytä tämän tilin kuvat ja videot galleriassa"</string> + <string name="add_account" msgid="4271217504968243974">"Lisää tili"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Valitse automaattisen lähetyksen tili"</string> +</resources> diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml new file mode 100644 index 000000000..cb6fd6ebe --- /dev/null +++ b/res/values-fr/strings.xml @@ -0,0 +1,172 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galerie"</string> + <string name="gadget_title" msgid="259405922673466798">"Cadre d\'image"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Lecteur Google Vidéos"</string> + <string name="loading_video" msgid="4013492720121891585">"Chargement de la vidéo..."</string> + <string name="loading_image" msgid="1200894415793838191">"Chargement de l\'image..."</string> + <string name="loading_account" msgid="928195413034552034">"Chargement infos compte..."</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Reprendre la vidéo"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Reprendre la lecture à partir de %s ?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Reprendre la lecture"</string> + <string name="loading" msgid="7038208555304563571">"Chargement en cours…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"Échec du chargement"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Aucune vignette"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Démarrer"</string> + <string name="crop_save_text" msgid="8821167985419282305">"OK"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Appuyez sur un visage pour commencer."</string> + <string name="saving_image" msgid="7270334453636349407">"Enregistrement de l\'image"</string> + <string name="crop_label" msgid="521114301871349328">"Rogner l\'image"</string> + <string name="select_image" msgid="7841406150484742140">"Sélectionner photo"</string> + <string name="select_video" msgid="4859510992798615076">"Sélectionner vidéo"</string> + <string name="select_item" msgid="2257529413100472599">"Sélection élément(s)"</string> + <string name="select_album" msgid="4632641262236697235">"Sélection album(s)"</string> + <string name="select_group" msgid="9090385962030340391">"Sélection groupe(s)"</string> + <string name="set_image" msgid="2331476809308010401">"Utiliser l\'image comme"</string> + <string name="wallpaper" msgid="9222901738515471972">"Configuration du fond d\'écran en cours. Veuillez patienter..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Fond d\'écran"</string> + <string name="delete" msgid="2839695998251824487">"Supprimer"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Confirmer la suppression"</string> + <string name="cancel" msgid="3637516880917356226">"Annuler"</string> + <string name="share" msgid="3619042788254195341">"Partager"</string> + <string name="select_all" msgid="8623593677101437957">"Tout sélectionner"</string> + <string name="deselect_all" msgid="7397531298370285581">"Tout désélectionner"</string> + <string name="slideshow" msgid="4355906903247112975">"Diaporama"</string> + <string name="details" msgid="8415120088556445230">"Détails"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Passer en mode Appareil photo"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"Afficher sur la carte"</string> + <string name="rotate_left" msgid="7412075232752726934">"Rotation à gauche"</string> + <string name="rotate_right" msgid="7340681085011826618">"Rotation à droite"</string> + <string name="no_such_item" msgid="3161074758669642065">"Élément introuvable"</string> + <string name="edit" msgid="1502273844748580847">"Modifier"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Aucune application disponible"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Demandes de mise en cache en cours de traitement"</string> + <string name="caching_label" msgid="3244800874547101776">"Mise en cache..."</string> + <string name="crop" msgid="7970750655414797277">"Rogner"</string> + <string name="set_as" msgid="3636764710790507868">"Définir comme"</string> + <string name="video_err" msgid="7917736494827857757">"Impossible de lire la vidéo."</string> + <string name="group_by_location" msgid="316641628989023253">"Par emplacement"</string> + <string name="group_by_time" msgid="9046168567717963573">"Par date"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Par tag"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Par personnes"</string> + <string name="group_by_album" msgid="1532818636053818958">"Par album"</string> + <string name="group_by_size" msgid="153766174950394155">"Par taille"</string> + <string name="untagged" msgid="7281481064509590402">"Aucun tag"</string> + <string name="no_location" msgid="2036710947563713111">"Aucune donnée de localisation"</string> + <string name="show_images_only" msgid="7263218480867672653">"Images uniquement"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Vidéos uniquement"</string> + <string name="show_all" msgid="4780647751652596980">"Images et vidéos"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Galerie photos"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Aucune photo"</string> + <string name="crop_saved" msgid="4684933379430649946">"Image rognée enregistrée dans les téléchargements"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"L\'image rognée n\'a pas été enregistrée."</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Aucun album disponible"</string> + <string name="empty_album" msgid="6307897398825514762">"Aucune image/vidéo disponible"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa Albums Web"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Consulter hors connexion"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"OK"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d élément(s) sur %2$d :"</string> + <string name="title" msgid="7622928349908052569">"Titre"</string> + <string name="description" msgid="3016729318096557520">"Description"</string> + <string name="time" msgid="1367953006052876956">"Heure"</string> + <string name="location" msgid="3432705876921618314">"Lieu"</string> + <string name="path" msgid="4725740395885105824">"Chemin d\'accès"</string> + <string name="width" msgid="9215847239714321097">"Largeur"</string> + <string name="height" msgid="3648885449443787772">"Hauteur"</string> + <string name="orientation" msgid="4958327983165245513">"Orientation"</string> + <string name="duration" msgid="8160058911218541616">"Durée"</string> + <string name="mimetype" msgid="3518268469266183548">"Type MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Taille fichier"</string> + <string name="maker" msgid="7921835498034236197">"Auteur"</string> + <string name="model" msgid="8240207064064337366">"Modèle"</string> + <string name="flash" msgid="2816779031261147723">"Flash"</string> + <string name="aperture" msgid="5920657630303915195">"Ouverture"</string> + <string name="focal_length" msgid="1291383769749877010">"Longueur focale"</string> + <string name="white_balance" msgid="8122534414851280901">"Balance blancs"</string> + <string name="exposure_time" msgid="3146642210127439553">"Temps exposition"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Manuel"</string> + <string name="auto" msgid="4296941368722892821">"Automatique"</string> + <string name="flash_on" msgid="7891556231891837284">"Flash déclenché"</string> + <string name="flash_off" msgid="1445443413822680010">"Flash désactivé"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Activation consultation album hors connexion"</item> + <item quantity="other" msgid="6929905722448632886">"Activation consultation albums hors connexion"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Cet élément est stocké localement et disponible hors connexion."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Tous les albums"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Albums stockés en local"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"Appareils MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Albums Picasa"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> disponible(s)"</string> + <string name="size_below" msgid="2074956730721942260">"jusqu\'à <xliff:g id="SIZE">%1$s</xliff:g>"</string> + <string name="size_above" msgid="5324398253474104087">"au-delà de <xliff:g id="SIZE">%1$s</xliff:g>"</string> + <string name="size_between" msgid="8779660840898917208">"de <xliff:g id="MIN_SIZE">%1$s</xliff:g> à <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Importer"</string> + <string name="import_complete" msgid="1098450310074640619">"Importation terminée"</string> + <string name="import_fail" msgid="5205927625132482529">"Échec de l\'importation."</string> + <string name="camera_connected" msgid="6984353643349303075">"Appareil photo connecté"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Appareil photo déconnecté"</string> + <string name="click_import" msgid="6407959065464291972">"Appuyez ici pour importer"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Images d\'un album"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Affichage aléatoire des images"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Choisir une photo"</string> + <string name="widget_type" msgid="7308564524449340985">"Type de widget"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Diaporama"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Prélecture des photos de Picasa :"</string> + <string name="cache_status" msgid="7690438435538533106">"Téléchargement de <xliff:g id="NUMBER_0">%1$s</xliff:g> photos sur <xliff:g id="NUMBER_1">%2$s</xliff:g>"</string> + <string name="cache_done" msgid="9194449192869777483">"Téléchargement terminé"</string> + <string name="albums" msgid="7320787705180057947">"Albums"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"Lieux"</string> + <string name="people" msgid="4114003823747292747">"Contacts"</string> + <string name="tags" msgid="5539648765482935955">"Tags"</string> + <string name="group_by" msgid="4308299657902209357">"Regrouper par"</string> + <string name="settings" msgid="1534847740615665736">"Paramètres"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"Paramètres du compte"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Paramètres d\'utilisation des données"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Transfert automatique"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Autres paramètres"</string> + <string name="about_gallery" msgid="8667445445883757255">"À propos de la galerie"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Synchronisation via Wi-Fi uniquement"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Transférez automatiquement toutes vos photos et vos vidéos dans un album Web Picasa privé."</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Activer le transfert automatique"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Synchro Google Photos ACTIVÉE"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Synchro Google Photos DÉSACTIVÉE"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Modifier préf. synchro ou supprimer compte"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Afficher les photos et vidéos de ce compte dans la galerie"</string> + <string name="add_account" msgid="4271217504968243974">"Ajouter un compte"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Sélect. compte transfert auto"</string> +</resources> diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml new file mode 100644 index 000000000..d84750486 --- /dev/null +++ b/res/values-hr/strings.xml @@ -0,0 +1,173 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galerija"</string> + <string name="gadget_title" msgid="259405922673466798">"Okvir slike"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Videoplayer"</string> + <string name="loading_video" msgid="4013492720121891585">"Učitavanje videozapisa…"</string> + <string name="loading_image" msgid="1200894415793838191">"Učitavanje slike…"</string> + <string name="loading_account" msgid="928195413034552034">"Učitavanje računa???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Nastavi videozapis"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Nastaviti reprodukciju od %s?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Nastavak reprodukcije"</string> + <string name="loading" msgid="7038208555304563571">"Učitavanje…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"Učitavanje nije uspjelo"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Nema minijatura"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Počni ispočetka"</string> + <string name="crop_save_text" msgid="8821167985419282305">"U redu"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Dotaknite lice za početak."</string> + <string name="saving_image" msgid="7270334453636349407">"Spremanje slike..."</string> + <string name="crop_label" msgid="521114301871349328">"Obrezivanje slike"</string> + <string name="select_image" msgid="7841406150484742140">"Odaberite fotog."</string> + <string name="select_video" msgid="4859510992798615076">"Odaberite videoz."</string> + <string name="select_item" msgid="2257529413100472599">"Odabir stavke(i)"</string> + <string name="select_album" msgid="4632641262236697235">"Odabir albuma"</string> + <string name="select_group" msgid="9090385962030340391">"Odabir skupine(a)"</string> + <string name="set_image" msgid="2331476809308010401">"Postavi sliku kao"</string> + <string name="wallpaper" msgid="9222901738515471972">"Postavljanje pozadinske slike, pričekajte..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Pozadinska slika"</string> + <string name="delete" msgid="2839695998251824487">"Izbriši"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Potvrdite brisanje"</string> + <string name="cancel" msgid="3637516880917356226">"Odustani"</string> + <string name="share" msgid="3619042788254195341">"Podijeli"</string> + <string name="select_all" msgid="8623593677101437957">"Odaberi sve"</string> + <string name="deselect_all" msgid="7397531298370285581">"Poništi odabir svih"</string> + <string name="slideshow" msgid="4355906903247112975">"Dijaprojekcija"</string> + <string name="details" msgid="8415120088556445230">"Pojedinosti"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Prebacivanje na fotoaparat"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"Pokaži na karti"</string> + <string name="rotate_left" msgid="7412075232752726934">"Rotiraj ulijevo"</string> + <string name="rotate_right" msgid="7340681085011826618">"Rotiraj udesno"</string> + <string name="no_such_item" msgid="3161074758669642065">"Stavka nije pronađena"</string> + <string name="edit" msgid="1502273844748580847">"Uredi"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Nema dostupne aplikacije"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Obrada zahtjeva za predmemoriju"</string> + <string name="caching_label" msgid="3244800874547101776">"Spremanje u priv. memoriju..."</string> + <string name="crop" msgid="7970750655414797277">"Obrezivanje"</string> + <string name="set_as" msgid="3636764710790507868">"Postavi kao"</string> + <string name="video_err" msgid="7917736494827857757">"Reprodukcija videozapisa nije moguća"</string> + <string name="group_by_location" msgid="316641628989023253">"Prema lokaciji"</string> + <string name="group_by_time" msgid="9046168567717963573">"Po vremenu"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Po oznakama"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Po osobama"</string> + <string name="group_by_album" msgid="1532818636053818958">"Po albumu"</string> + <string name="group_by_size" msgid="153766174950394155">"Po veličini"</string> + <string name="untagged" msgid="7281481064509590402">"Neoznačeno"</string> + <string name="no_location" msgid="2036710947563713111">"Nema lokacije"</string> + <string name="show_images_only" msgid="7263218480867672653">"Samo slike"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Samo videozapisi"</string> + <string name="show_all" msgid="4780647751652596980">"Slike i videozapisi"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Galerija fotografija"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Nema fotografija"</string> + <string name="crop_saved" msgid="4684933379430649946">"Izrezana slika spremljena je u preuzimanju"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Izrezana slika nije spremljena"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Nema dostupnih albuma"</string> + <string name="empty_album" msgid="6307897398825514762">"Nema dostupnih slika/videozapisa"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa web-albumi"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Učini dostupnim van mreže"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Gotovo"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d od %2$d stavki:"</string> + <string name="title" msgid="7622928349908052569">"Naslov"</string> + <string name="description" msgid="3016729318096557520">"Opis"</string> + <string name="time" msgid="1367953006052876956">"Vrijeme"</string> + <string name="location" msgid="3432705876921618314">"Lokacija"</string> + <string name="path" msgid="4725740395885105824">"Putanja"</string> + <string name="width" msgid="9215847239714321097">"Širina"</string> + <string name="height" msgid="3648885449443787772">"Visina"</string> + <string name="orientation" msgid="4958327983165245513">"Usmjerenje"</string> + <string name="duration" msgid="8160058911218541616">"Trajanje"</string> + <string name="mimetype" msgid="3518268469266183548">"Vrsta MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Vel. datoteke"</string> + <string name="maker" msgid="7921835498034236197">"Autor"</string> + <string name="model" msgid="8240207064064337366">"Model"</string> + <string name="flash" msgid="2816779031261147723">"Flash"</string> + <string name="aperture" msgid="5920657630303915195">"Otvor blende"</string> + <string name="focal_length" msgid="1291383769749877010">"Žariš. duljina"</string> + <string name="white_balance" msgid="8122534414851280901">"Uravn. bijelog"</string> + <string name="exposure_time" msgid="3146642210127439553">"Vrijeme izlag."</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Ručno"</string> + <string name="auto" msgid="4296941368722892821">"Automatski"</string> + <string name="flash_on" msgid="7891556231891837284">"Bljes. okinuta"</string> + <string name="flash_off" msgid="1445443413822680010">"Bez bljesk."</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Spremanje albuma za izvanmrežni rad"</item> + <item quantity="other" msgid="6929905722448632886">"Spremanje albuma za izvanmrežni rad"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Ova stavka pohranjena je lokalno i dostupna je izvan mreže."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Svi albumi"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Lokalni albumi"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"MTP uređaji"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Picasa albumi"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> slobodno"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> ili niže"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> ili više"</string> + <string name="size_between" msgid="8779660840898917208">"Od <xliff:g id="MIN_SIZE">%1$s</xliff:g> do <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Uvezi"</string> + <string name="import_complete" msgid="1098450310074640619">"Uvoz je dovršen"</string> + <string name="import_fail" msgid="5205927625132482529">"Uvoz nije uspio"</string> + <string name="camera_connected" msgid="6984353643349303075">"Fotoaparat je uključen"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Fotoaparat je isključen"</string> + <string name="click_import" msgid="6407959065464291972">"Dodirnite ovdje za uvoz"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Slike iz albuma"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Nasumično prikaži sve slike"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Odaberi sliku"</string> + <string name="widget_type" msgid="7308564524449340985">"Vrsta widgeta"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Dijaprojekcija"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Pretpreuzimanje Picasa fotografija:"</string> + <string name="cache_status" msgid="7690438435538533106">"Preuzmite <xliff:g id="NUMBER_0">%1$s</xliff:g> od fotografija: <xliff:g id="NUMBER_1">%2$s</xliff:g>"</string> + <string name="cache_done" msgid="9194449192869777483">"Preuzimanje je dovršeno"</string> + <string name="albums" msgid="7320787705180057947">"Albumi"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"Lokacije"</string> + <string name="people" msgid="4114003823747292747">"Osobe"</string> + <string name="tags" msgid="5539648765482935955">"Oznake"</string> + <string name="group_by" msgid="4308299657902209357">"Grupiraj po"</string> + <!-- no translation found for settings (1534847740615665736) --> + <skip /> + <string name="prefs_accounts" msgid="7942761992713671670">"Postavke računa"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Postavke upotrebe podataka"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Automatski prijenos"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Ostale postavke"</string> + <string name="about_gallery" msgid="8667445445883757255">"O galeriji"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Sinkronizacija samo na WiFi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Automatski prenesite sve fotografije i videozapise koje snimite u privatni Picasa web-album"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Omogući automatski prijenos"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Sinkr. Google fotogr. uključena"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Sinkr. Google fotogr. isključena"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Izmijeni postavke sink. ili ukloni račun"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Pregled fotografija i videozapisa s ovog računa u Galeriji"</string> + <string name="add_account" msgid="4271217504968243974">"Dodaj račun"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Odabir račun za automatski prijenos"</string> +</resources> diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml new file mode 100644 index 000000000..11c19cc20 --- /dev/null +++ b/res/values-hu/strings.xml @@ -0,0 +1,177 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galéria"</string> + <string name="gadget_title" msgid="259405922673466798">"Képkeret"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Videolejátszó"</string> + <string name="loading_video" msgid="4013492720121891585">"Videó betöltése…"</string> + <string name="loading_image" msgid="1200894415793838191">"Kép betöltése..."</string> + <string name="loading_account" msgid="928195413034552034">"Fiók betöltése..."</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Videó folytatása"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Folytatja a lejátszást innen: %s ?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Lejátszás folytatása"</string> + <string name="loading" msgid="7038208555304563571">"Betöltés…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"A betöltés nem sikerült"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Nincs indexkép"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Újrakezdés"</string> + <string name="crop_save_text" msgid="8821167985419282305">"OK"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"A kezdéshez érintse meg az egyik arcot."</string> + <string name="saving_image" msgid="7270334453636349407">"Kép mentése..."</string> + <string name="crop_label" msgid="521114301871349328">"Kép levágása"</string> + <string name="select_image" msgid="7841406150484742140">"Fénykép kiválasztása"</string> + <string name="select_video" msgid="4859510992798615076">"Videó kiválasztása"</string> + <string name="select_item" msgid="2257529413100472599">"Elem(ek) választása"</string> + <string name="select_album" msgid="4632641262236697235">"Album(ok) választása"</string> + <string name="select_group" msgid="9090385962030340391">"Csoport(ok) kivál."</string> + <string name="set_image" msgid="2331476809308010401">"Kép beállítása, mint"</string> + <string name="wallpaper" msgid="9222901738515471972">"Háttérkép beállítása, kérjük, várjon..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Háttérkép"</string> + <string name="delete" msgid="2839695998251824487">"Törlés"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Törlés megerősítése"</string> + <string name="cancel" msgid="3637516880917356226">"Mégse"</string> + <string name="share" msgid="3619042788254195341">"Megosztás"</string> + <string name="select_all" msgid="8623593677101437957">"Összes kijelölése"</string> + <string name="deselect_all" msgid="7397531298370285581">"Összes kijelölés törlése"</string> + <string name="slideshow" msgid="4355906903247112975">"Diavetítés"</string> + <string name="details" msgid="8415120088556445230">"Részletek"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Váltás kamerára"</string> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"%1$d kiválasztva"</item> + <item quantity="one" msgid="2478365152745637768">"%1$d kiválasztva"</item> + <item quantity="other" msgid="754722656147810487">"%1$d kiválasztva"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"%1$d kiválasztva"</item> + <item quantity="one" msgid="6184377003099987825">"%1$d kiválasztva"</item> + <item quantity="other" msgid="53105607141906130">"%1$d kiválasztva"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"%1$d kiválasztva"</item> + <item quantity="one" msgid="5030162638216034260">"%1$d kiválasztva"</item> + <item quantity="other" msgid="3512041363942842738">"%1$d kiválasztva"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"Megjelenítés a térképen"</string> + <string name="rotate_left" msgid="7412075232752726934">"Forgatás balra"</string> + <string name="rotate_right" msgid="7340681085011826618">"Forgatás jobbra"</string> + <string name="no_such_item" msgid="3161074758669642065">"Az elem nem található"</string> + <string name="edit" msgid="1502273844748580847">"Szerkesztés"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Nincs használható alkalmazás"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Tárolási kérelmek feldolgozása"</string> + <string name="caching_label" msgid="3244800874547101776">"Gyorsítótárazás ..."</string> + <string name="crop" msgid="7970750655414797277">"Levágás"</string> + <string name="set_as" msgid="3636764710790507868">"Beállítás, mint"</string> + <string name="video_err" msgid="7917736494827857757">"Nem lehet lejátszani a videót"</string> + <string name="group_by_location" msgid="316641628989023253">"Helyszín szerint"</string> + <string name="group_by_time" msgid="9046168567717963573">"Idő szerint"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Címkék szerint"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Arcok alapján"</string> + <string name="group_by_album" msgid="1532818636053818958">"Album szerint"</string> + <string name="group_by_size" msgid="153766174950394155">"Méret szerint"</string> + <string name="untagged" msgid="7281481064509590402">"Címke nélküli"</string> + <string name="no_location" msgid="2036710947563713111">"Nincs helyadat"</string> + <string name="show_images_only" msgid="7263218480867672653">"Csak képek"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Csak videók"</string> + <string name="show_all" msgid="4780647751652596980">"Képek és videók"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Fotógaléria"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Nincsenek fotók"</string> + <string name="crop_saved" msgid="4684933379430649946">"A levágott kép elmentve a letöltések közé"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"A levágott kép nincs elmentve"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Nincs rendelkezésre álló album"</string> + <string name="empty_album" msgid="6307897398825514762">"Nincsenek elérhető képek/videók"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa Webalbumok"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Zümm"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Offline elérhető albumok"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Kész"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%2$d/%1$d elem:"</string> + <string name="title" msgid="7622928349908052569">"Beosztás"</string> + <string name="description" msgid="3016729318096557520">"Leírás"</string> + <string name="time" msgid="1367953006052876956">"Idő"</string> + <string name="location" msgid="3432705876921618314">"Hely"</string> + <string name="path" msgid="4725740395885105824">"Elérési út"</string> + <string name="width" msgid="9215847239714321097">"Szélesség"</string> + <string name="height" msgid="3648885449443787772">"Magasság"</string> + <string name="orientation" msgid="4958327983165245513">"Tájolás"</string> + <string name="duration" msgid="8160058911218541616">"Időtartam"</string> + <string name="mimetype" msgid="3518268469266183548">"MIME típus"</string> + <string name="file_size" msgid="4670384449129762138">"Fájl mérete"</string> + <string name="maker" msgid="7921835498034236197">"Készítő"</string> + <string name="model" msgid="8240207064064337366">"Modell"</string> + <string name="flash" msgid="2816779031261147723">"Vaku"</string> + <string name="aperture" msgid="5920657630303915195">"Rekesz"</string> + <string name="focal_length" msgid="1291383769749877010">"Fókusztávolság"</string> + <string name="white_balance" msgid="8122534414851280901">"Fehéregyensúly"</string> + <string name="exposure_time" msgid="3146642210127439553">"Expozíciós idő"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Kézi"</string> + <string name="auto" msgid="4296941368722892821">"Automata"</string> + <string name="flash_on" msgid="7891556231891837284">"Vakuvillanás"</string> + <string name="flash_off" msgid="1445443413822680010">"Vaku nélkül"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Album letöltése offline hallgatáshoz"</item> + <item quantity="other" msgid="6929905722448632886">"Albumok letöltése offline hallgatáshoz"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Az elemet a készülék helyileg tárolta, és elérhető offline módban."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Összes album"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Helyi albumok"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"MTP eszközök"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Picasa albumok"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> szabad"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> vagy kevesebb"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> vagy több"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> - <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Importálás"</string> + <string name="import_complete" msgid="1098450310074640619">"Importálás befejezve"</string> + <string name="import_fail" msgid="5205927625132482529">"Az importálás sikertelen"</string> + <string name="camera_connected" msgid="6984353643349303075">"Kamera csatlakoztatva"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Kamera leválasztva"</string> + <string name="click_import" msgid="6407959065464291972">"Importáláshoz érintse meg itt"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Képek egy albumból"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Az összes kép váltása"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Válasszon egy képet"</string> + <string name="widget_type" msgid="7308564524449340985">"Modul típusa"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Diavetítés"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Picasa-fényképek előzetes lekérése"</string> + <string name="cache_status" msgid="7690438435538533106">"<xliff:g id="NUMBER_0">%1$s</xliff:g>/<xliff:g id="NUMBER_1">%2$s</xliff:g> fotó letöltése"</string> + <string name="cache_done" msgid="9194449192869777483">"A letöltés befejeződött"</string> + <string name="albums" msgid="7320787705180057947">"Albumok"</string> + <string name="times" msgid="2023033894889499219">"Alkalom"</string> + <string name="locations" msgid="6649297994083130305">"Helyek"</string> + <string name="people" msgid="4114003823747292747">"Személyek"</string> + <string name="tags" msgid="5539648765482935955">"Címkék"</string> + <string name="group_by" msgid="4308299657902209357">"Csoportosítás"</string> + <string name="settings" msgid="1534847740615665736">"Beállítások"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"Fiókbeállítások"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Adathasználati beállítások"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Automatikus feltöltés"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Egyéb beállítások"</string> + <string name="about_gallery" msgid="8667445445883757255">"A galéria névjegye"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Szinkronizálás csak Wi-Fin"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"A készített fotók és videók automatikus feltöltése egy személyes Picasa webalbumba"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Automatikus feltöltés engedélyezése"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Google fotók szinkr. BE"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Google fotók szinkr. KI"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Szinkr. beáll. mód. vagy a fiók törlése"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"A fiókban tárolt fotók és videók megtekintése a galériában"</string> + <string name="add_account" msgid="4271217504968243974">"Fiók hozzáadása"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Aut. feltöltési fiók kivál."</string> +</resources> diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml new file mode 100644 index 000000000..08d4e6c07 --- /dev/null +++ b/res/values-in/strings.xml @@ -0,0 +1,178 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galeri"</string> + <string name="gadget_title" msgid="259405922673466798">"Bingkai gambar"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Pemutar video"</string> + <string name="loading_video" msgid="4013492720121891585">"Memuat video..."</string> + <string name="loading_image" msgid="1200894415793838191">"Memuat gambar…"</string> + <string name="loading_account" msgid="928195413034552034">"Memuat akun???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Lanjutkan video"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Lanjutkan pemutaran dari %s ?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Lanjutkan pemutaran"</string> + <string name="loading" msgid="7038208555304563571">"Memuat…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"Gagal dimuat"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Tidak ada gambar mini"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Memulai"</string> + <string name="crop_save_text" msgid="8821167985419282305">"Ok"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Ketuk wajah untuk memulai."</string> + <string name="saving_image" msgid="7270334453636349407">"Menyimpan gambar…"</string> + <string name="crop_label" msgid="521114301871349328">"Pangkas gambar"</string> + <string name="select_image" msgid="7841406150484742140">"Pilih foto"</string> + <string name="select_video" msgid="4859510992798615076">"Pilih video"</string> + <string name="select_item" msgid="2257529413100472599">"Pilih item"</string> + <string name="select_album" msgid="4632641262236697235">"Pilih album"</string> + <string name="select_group" msgid="9090385962030340391">"Pilih grup"</string> + <string name="set_image" msgid="2331476809308010401">"Setel gambar sebagai"</string> + <string name="wallpaper" msgid="9222901738515471972">"Menyetel wallpaper, harap tunggu..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Wallpaper"</string> + <string name="delete" msgid="2839695998251824487">"Hapus"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Konfirmasi Hapus"</string> + <string name="cancel" msgid="3637516880917356226">"Batal"</string> + <string name="share" msgid="3619042788254195341">"Bagikan"</string> + <string name="select_all" msgid="8623593677101437957">"Pilih Semua"</string> + <string name="deselect_all" msgid="7397531298370285581">"Batalkan semua pilihan"</string> + <string name="slideshow" msgid="4355906903247112975">"Rangkai salindia"</string> + <string name="details" msgid="8415120088556445230">"Detail"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Beralih ke Kamera"</string> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"%1$d dipilih"</item> + <item quantity="one" msgid="2478365152745637768">"%1$d dipilih"</item> + <item quantity="other" msgid="754722656147810487">"%1$d dipilih"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"%1$d dipilih"</item> + <item quantity="one" msgid="6184377003099987825">"%1$d dipilih"</item> + <item quantity="other" msgid="53105607141906130">"%1$d dipilih"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"%1$d dipilih"</item> + <item quantity="one" msgid="5030162638216034260">"%1$d dipilih"</item> + <item quantity="other" msgid="3512041363942842738">"%1$d dipilih"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"Tampilkan pada peta"</string> + <string name="rotate_left" msgid="7412075232752726934">"Putar ke Kiri"</string> + <string name="rotate_right" msgid="7340681085011826618">"Putar ke Kanan"</string> + <string name="no_such_item" msgid="3161074758669642065">"Item tidak ditemukan"</string> + <string name="edit" msgid="1502273844748580847">"Edit"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Aplikasi tidak tersedia"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Memproses Permintaan Penyimpanan dalam Tembolok"</string> + <string name="caching_label" msgid="3244800874547101776">"Menyimpan ke tembolok..."</string> + <string name="crop" msgid="7970750655414797277">"Pangkas"</string> + <string name="set_as" msgid="3636764710790507868">"Setel sebagai"</string> + <string name="video_err" msgid="7917736494827857757">"Tidak dapat memutar video"</string> + <string name="group_by_location" msgid="316641628989023253">"Menurut lokasi"</string> + <string name="group_by_time" msgid="9046168567717963573">"Menurut waktu"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Menurut tag"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Menurut orang"</string> + <string name="group_by_album" msgid="1532818636053818958">"Menurut album"</string> + <string name="group_by_size" msgid="153766174950394155">"Menurut ukuran"</string> + <string name="untagged" msgid="7281481064509590402">"Tidak di-tag"</string> + <string name="no_location" msgid="2036710947563713111">"Tidak Ada Lokasi"</string> + <string name="show_images_only" msgid="7263218480867672653">"Hanya gambar"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Hanya video"</string> + <string name="show_all" msgid="4780647751652596980">"Gambar dan video"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Galeri Foto"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Tidak Ada Foto"</string> + <string name="crop_saved" msgid="4684933379430649946">"Gambar yang dipangkas telah disimpan dalam unduhan"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Gambar yang dipangkas tidak disimpan"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Album tidak tersedia"</string> + <string name="empty_album" msgid="6307897398825514762">"Gambar/video tidak tersedia"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Album Web Picasa"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Jadikan agar tersedia luring"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Selesai"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d dari %2$d item:"</string> + <string name="title" msgid="7622928349908052569">"Judul"</string> + <string name="description" msgid="3016729318096557520">"Uraian"</string> + <string name="time" msgid="1367953006052876956">"Waktu"</string> + <string name="location" msgid="3432705876921618314">"Lokasi"</string> + <string name="path" msgid="4725740395885105824">"Jalur"</string> + <string name="width" msgid="9215847239714321097">"Lebar"</string> + <string name="height" msgid="3648885449443787772">"Tinggi"</string> + <string name="orientation" msgid="4958327983165245513">"Orientasi"</string> + <string name="duration" msgid="8160058911218541616">"Durasi"</string> + <string name="mimetype" msgid="3518268469266183548">"Jenis MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Ukuran Berkas"</string> + <string name="maker" msgid="7921835498034236197">"Pembuat"</string> + <string name="model" msgid="8240207064064337366">"Model"</string> + <string name="flash" msgid="2816779031261147723">"Lampu Kilat"</string> + <string name="aperture" msgid="5920657630303915195">"Bukaan Diafragma"</string> + <string name="focal_length" msgid="1291383769749877010">"Jarak Fokus"</string> + <string name="white_balance" msgid="8122534414851280901">"Keseimbangan Putih"</string> + <string name="exposure_time" msgid="3146642210127439553">"Waktu Pemaparan"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Manual"</string> + <string name="auto" msgid="4296941368722892821">"Otomatis"</string> + <string name="flash_on" msgid="7891556231891837284">"Lampu kilat aktif"</string> + <string name="flash_off" msgid="1445443413822680010">"Tanpa lampu kilat"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Menjadikan album tersedia secara luring"</item> + <item quantity="other" msgid="6929905722448632886">"Menjadikan album tersedia secara luring"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Item ini tersimpan secara lokal dan tersedia secara luring."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Semua Album"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Album Lokal"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"Perangkat MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Album Picasa"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> bebas"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> atau kurang"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> atau lebih"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> hingga <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Impor"</string> + <string name="import_complete" msgid="1098450310074640619">"Impor Selesai"</string> + <string name="import_fail" msgid="5205927625132482529">"Impor Gagal"</string> + <string name="camera_connected" msgid="6984353643349303075">"Kamera tersambung"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Kamera terputus"</string> + <string name="click_import" msgid="6407959065464291972">"Sentuh di sini untuk mengimpor"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Gambar dari album"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Kocok semua gambar"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Pilih gambar"</string> + <string name="widget_type" msgid="7308564524449340985">"Jenis Gawit"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Rangkai salindia"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Mengambil foto picasa lebih dulu:"</string> + <string name="cache_status" msgid="7690438435538533106">"Mengunduh <xliff:g id="NUMBER_0">%1$s</xliff:g> dari <xliff:g id="NUMBER_1">%2$s</xliff:g> foto"</string> + <string name="cache_done" msgid="9194449192869777483">"Unduhan selesai"</string> + <string name="albums" msgid="7320787705180057947">"Album"</string> + <string name="times" msgid="2023033894889499219">"Waktu"</string> + <string name="locations" msgid="6649297994083130305">"Lokasi"</string> + <string name="people" msgid="4114003823747292747">"Orang"</string> + <string name="tags" msgid="5539648765482935955">"Tag"</string> + <string name="group_by" msgid="4308299657902209357">"Kelompokkan menurut"</string> + <!-- no translation found for settings (1534847740615665736) --> + <skip /> + <string name="prefs_accounts" msgid="7942761992713671670">"Setelan akun"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Setelan penggunaan data"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Unggah-otomatis"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Setelan lainnya"</string> + <string name="about_gallery" msgid="8667445445883757255">"Tentang Galeri"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Sinkronkan pada WiFi saja"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Otomatis mengunggah semua foto dan video yang Anda ambil ke album web picasa pribadi"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Aktifkan Unggah-otomatis"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Sinkronisasi foto Google NYALA"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Sinkronisasi foto Google MATI"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Ubah pfrnsi snkrnisasi atau hps akun ini"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Lihat foto dan video dari akun ini di Galeri"</string> + <string name="add_account" msgid="4271217504968243974">"Tambah akun"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Pilih akun Unggah-otomatis"</string> +</resources> diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml new file mode 100644 index 000000000..7abc2af71 --- /dev/null +++ b/res/values-it/strings.xml @@ -0,0 +1,177 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galleria"</string> + <string name="gadget_title" msgid="259405922673466798">"Cornice immagine"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Video player"</string> + <string name="loading_video" msgid="4013492720121891585">"Caricamento video..."</string> + <string name="loading_image" msgid="1200894415793838191">"Caricamento immagine in corso..."</string> + <string name="loading_account" msgid="928195413034552034">"Caricamento account???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Riprendi video"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Riprendi riproduzione da %s ?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Riprendi riproduzione"</string> + <string name="loading" msgid="7038208555304563571">"Caricamento in corso..."</string> + <string name="fail_to_load" msgid="2710120770735315683">"Caricamento non riuscito"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Nessuna miniatura"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Ricomincia"</string> + <string name="crop_save_text" msgid="8821167985419282305">"OK"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Tocca un viso per iniziare."</string> + <string name="saving_image" msgid="7270334453636349407">"Salvataggio foto in corso..."</string> + <string name="crop_label" msgid="521114301871349328">"Ritaglia foto"</string> + <string name="select_image" msgid="7841406150484742140">"Seleziona foto"</string> + <string name="select_video" msgid="4859510992798615076">"Seleziona video"</string> + <string name="select_item" msgid="2257529413100472599">"Seleziona elementi"</string> + <string name="select_album" msgid="4632641262236697235">"Seleziona album"</string> + <string name="select_group" msgid="9090385962030340391">"Seleziona gruppi"</string> + <string name="set_image" msgid="2331476809308010401">"Imposta foto come"</string> + <string name="wallpaper" msgid="9222901738515471972">"Impostazione sfondo, attendi..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Sfondo"</string> + <string name="delete" msgid="2839695998251824487">"Elimina"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Conferma eliminazione"</string> + <string name="cancel" msgid="3637516880917356226">"Annulla"</string> + <string name="share" msgid="3619042788254195341">"Condividi"</string> + <string name="select_all" msgid="8623593677101437957">"Seleziona tutto"</string> + <string name="deselect_all" msgid="7397531298370285581">"Deseleziona tutto"</string> + <string name="slideshow" msgid="4355906903247112975">"Presentazione"</string> + <string name="details" msgid="8415120088556445230">"Dettagli"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Passa a Fotocamera"</string> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"%1$d selezionati"</item> + <item quantity="one" msgid="2478365152745637768">"%1$d selezionato"</item> + <item quantity="other" msgid="754722656147810487">"%1$d selezionati"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"%1$d selezionati"</item> + <item quantity="one" msgid="6184377003099987825">"%1$d selezionato"</item> + <item quantity="other" msgid="53105607141906130">"%1$d selezionati"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"%1$d selezionati"</item> + <item quantity="one" msgid="5030162638216034260">"%1$d selezionato"</item> + <item quantity="other" msgid="3512041363942842738">"%1$d selezionati"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"Mostra sulla mappa"</string> + <string name="rotate_left" msgid="7412075232752726934">"Ruota a sinistra"</string> + <string name="rotate_right" msgid="7340681085011826618">"Ruota a destra"</string> + <string name="no_such_item" msgid="3161074758669642065">"Elemento non trovato"</string> + <string name="edit" msgid="1502273844748580847">"Modifica"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Nessuna applicazione disponibile"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Elabora richieste memorizzazione nella cache"</string> + <string name="caching_label" msgid="3244800874547101776">"Memorizz. in cache"</string> + <string name="crop" msgid="7970750655414797277">"Ritaglia"</string> + <string name="set_as" msgid="3636764710790507868">"Imposta come"</string> + <string name="video_err" msgid="7917736494827857757">"Impossibile riprodurre il video"</string> + <string name="group_by_location" msgid="316641628989023253">"Per luogo"</string> + <string name="group_by_time" msgid="9046168567717963573">"Per data"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Per tag"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Per persone"</string> + <string name="group_by_album" msgid="1532818636053818958">"Per album"</string> + <string name="group_by_size" msgid="153766174950394155">"Per dimensioni"</string> + <string name="untagged" msgid="7281481064509590402">"Senza tag"</string> + <string name="no_location" msgid="2036710947563713111">"Nessun luogo"</string> + <string name="show_images_only" msgid="7263218480867672653">"Solo immagini"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Solo video"</string> + <string name="show_all" msgid="4780647751652596980">"Immagini e video"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Galleria fotografica"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Nessuna foto"</string> + <string name="crop_saved" msgid="4684933379430649946">"L\'immagine ritagliata è stata salvata in download"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"L\'immagine ritagliata non è stata salvata"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Nessun album disponibile"</string> + <string name="empty_album" msgid="6307897398825514762">"Non ci sono immagini/video disponibili"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa Web Album"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Rendi disponibili offline"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Fine"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d su %2$d elementi :"</string> + <string name="title" msgid="7622928349908052569">"Titolo"</string> + <string name="description" msgid="3016729318096557520">"Descrizione"</string> + <string name="time" msgid="1367953006052876956">"Ora"</string> + <string name="location" msgid="3432705876921618314">"Luogo"</string> + <string name="path" msgid="4725740395885105824">"Percorso"</string> + <string name="width" msgid="9215847239714321097">"Larghezza"</string> + <string name="height" msgid="3648885449443787772">"Altezza"</string> + <string name="orientation" msgid="4958327983165245513">"Orientamento"</string> + <string name="duration" msgid="8160058911218541616">"Durata"</string> + <string name="mimetype" msgid="3518268469266183548">"Tipo MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Dimensioni file"</string> + <string name="maker" msgid="7921835498034236197">"Autore"</string> + <string name="model" msgid="8240207064064337366">"Modello"</string> + <string name="flash" msgid="2816779031261147723">"Flash"</string> + <string name="aperture" msgid="5920657630303915195">"Diaframma"</string> + <string name="focal_length" msgid="1291383769749877010">"Lungh. focale"</string> + <string name="white_balance" msgid="8122534414851280901">"Bilanc. bianco"</string> + <string name="exposure_time" msgid="3146642210127439553">"Tempo esposiz."</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Manuale"</string> + <string name="auto" msgid="4296941368722892821">"Autom."</string> + <string name="flash_on" msgid="7891556231891837284">"Flash scattato"</string> + <string name="flash_off" msgid="1445443413822680010">"Senza flash"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Attivazione album offline"</item> + <item quantity="other" msgid="6929905722448632886">"Attivazione album offline"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Questo elemento è memorizzato localmente e disponibile offline."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Tutti gli album"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Album locali"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"Dispositivi MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Album di Picasa"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> liberi"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> o minore"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> o maggiore"</string> + <string name="size_between" msgid="8779660840898917208">"Da <xliff:g id="MIN_SIZE">%1$s</xliff:g> a <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Importa"</string> + <string name="import_complete" msgid="1098450310074640619">"Importazione completa"</string> + <string name="import_fail" msgid="5205927625132482529">"Importazione non riuscita"</string> + <string name="camera_connected" msgid="6984353643349303075">"Fotocamera collegata"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Fotocamera scollegata"</string> + <string name="click_import" msgid="6407959065464291972">"Tocca qui per importare"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Immagini da un album"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Visualizzazione casuale immagini"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Scegli un\'immagine"</string> + <string name="widget_type" msgid="7308564524449340985">"Tipo di widget"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Presentazione"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Precaricamento delle foto di Picasa:"</string> + <string name="cache_status" msgid="7690438435538533106">"Download di <xliff:g id="NUMBER_0">%1$s</xliff:g> di <xliff:g id="NUMBER_1">%2$s</xliff:g> foto"</string> + <string name="cache_done" msgid="9194449192869777483">"Download completato"</string> + <string name="albums" msgid="7320787705180057947">"Album"</string> + <string name="times" msgid="2023033894889499219">"Volte"</string> + <string name="locations" msgid="6649297994083130305">"Luoghi"</string> + <string name="people" msgid="4114003823747292747">"Persone"</string> + <string name="tags" msgid="5539648765482935955">"Tag"</string> + <string name="group_by" msgid="4308299657902209357">"Raggruppa per"</string> + <string name="settings" msgid="1534847740615665736">"Impostazioni"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"Impostazioni account"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Impostazioni utilizzo dati"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Caricamento automatico"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Altre impostazioni"</string> + <string name="about_gallery" msgid="8667445445883757255">"Informazioni su Galleria"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Sincronizza solo su Wi-Fi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Carica automaticamente tutte le foto e tutti i video realizzati in un album privato di Picasa Web Album"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Attiva caricamento automatico"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Sincronizz. foto Google attiva"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Sincron. foto Google non attiva"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Cambia prefer. sincron. o rimuovi account"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Visualizza foto e video di questo account nella Galleria"</string> + <string name="add_account" msgid="4271217504968243974">"Aggiungi account"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Scegli account caricam. autom."</string> +</resources> diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml new file mode 100644 index 000000000..af1c8499a --- /dev/null +++ b/res/values-iw/strings.xml @@ -0,0 +1,172 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"גלריה"</string> + <string name="gadget_title" msgid="259405922673466798">"מסגרת תמונה"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Google Video Player"</string> + <string name="loading_video" msgid="4013492720121891585">"טוען סרטון וידאו…"</string> + <string name="loading_image" msgid="1200894415793838191">"טוען תמונה…"</string> + <string name="loading_account" msgid="928195413034552034">"טוען חשבון???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"המשך את הקרנת סרטון הווידאו"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"להמשיך להפעיל מ-%s ?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"המשך את ההפעלה"</string> + <string name="loading" msgid="7038208555304563571">"טוען…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"הטעינה נכשלה"</string> + <string name="no_thumbnail" msgid="284723185546429750">"ללא תמונה ממוזערת"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"התחל מחדש"</string> + <string name="crop_save_text" msgid="8821167985419282305">"אישור"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"הקש על פנים כדי להתחיל."</string> + <string name="saving_image" msgid="7270334453636349407">"שומר תמונה..."</string> + <string name="crop_label" msgid="521114301871349328">"חתוך תמונה"</string> + <string name="select_image" msgid="7841406150484742140">"בחר תמונה"</string> + <string name="select_video" msgid="4859510992798615076">"בחר סרטון"</string> + <string name="select_item" msgid="2257529413100472599">"בחר פריטים"</string> + <string name="select_album" msgid="4632641262236697235">"בחר אלבומים"</string> + <string name="select_group" msgid="9090385962030340391">"בחר קבוצות"</string> + <string name="set_image" msgid="2331476809308010401">"הגדר תמונה בתור"</string> + <string name="wallpaper" msgid="9222901738515471972">"מגדיר טפט, נא המתן..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"טפט"</string> + <string name="delete" msgid="2839695998251824487">"מחק"</string> + <string name="confirm_delete" msgid="5731757674837098707">"אשר מחיקה"</string> + <string name="cancel" msgid="3637516880917356226">"ביטול"</string> + <string name="share" msgid="3619042788254195341">"שתף"</string> + <string name="select_all" msgid="8623593677101437957">"בחר הכל"</string> + <string name="deselect_all" msgid="7397531298370285581">"בטל את הבחירה של כולם"</string> + <string name="slideshow" msgid="4355906903247112975">"הצגת שקופיות"</string> + <string name="details" msgid="8415120088556445230">"פרטים"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"עבור למצלמה"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"הצג במפה"</string> + <string name="rotate_left" msgid="7412075232752726934">"סובב שמאלה"</string> + <string name="rotate_right" msgid="7340681085011826618">"סובב ימינה"</string> + <string name="no_such_item" msgid="3161074758669642065">"הפריט לא נמצא"</string> + <string name="edit" msgid="1502273844748580847">"ערוך"</string> + <string name="activity_not_found" msgid="3731390759313019518">"אין יישום זמין"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"עבד בקשות העברה לקובץ שמור"</string> + <string name="caching_label" msgid="3244800874547101776">"שומר בקובץ..."</string> + <string name="crop" msgid="7970750655414797277">"חתוך"</string> + <string name="set_as" msgid="3636764710790507868">"הגדר בתור"</string> + <string name="video_err" msgid="7917736494827857757">"אין אפשרות להפעיל את סרטון הווידאו"</string> + <string name="group_by_location" msgid="316641628989023253">"לפי מיקום"</string> + <string name="group_by_time" msgid="9046168567717963573">"לפי זמן"</string> + <string name="group_by_tags" msgid="3568731317210676160">"לפי תגים"</string> + <string name="group_by_faces" msgid="1566351636227274906">"לפי אנשים"</string> + <string name="group_by_album" msgid="1532818636053818958">"לפי אלבום"</string> + <string name="group_by_size" msgid="153766174950394155">"לפי גודל"</string> + <string name="untagged" msgid="7281481064509590402">"ללא תג"</string> + <string name="no_location" msgid="2036710947563713111">"ללא מיקום"</string> + <string name="show_images_only" msgid="7263218480867672653">"תמונות בלבד"</string> + <string name="show_videos_only" msgid="3850394623678871697">"סרטוני וידאו בלבד"</string> + <string name="show_all" msgid="4780647751652596980">"תמונות וסרטוני וידאו"</string> + <string name="appwidget_title" msgid="6410561146863700411">"גלריית תמונות"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"אין תצלומים"</string> + <string name="crop_saved" msgid="4684933379430649946">"התמונה החתוכה נשמרה בהורדה"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"התמונה החתוכה לא נשמרה"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"אין אלבומים זמינים"</string> + <string name="empty_album" msgid="6307897398825514762">"אין תמונות/סרטוני וידאו זמינים"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"אלבומי Google"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"הפוך לזמין באופן לא מקוון"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"בוצע"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d מתוך %2$d פריטים:"</string> + <string name="title" msgid="7622928349908052569">"כותרת"</string> + <string name="description" msgid="3016729318096557520">"תיאור"</string> + <string name="time" msgid="1367953006052876956">"שעה"</string> + <string name="location" msgid="3432705876921618314">"מיקום"</string> + <string name="path" msgid="4725740395885105824">"נתיב"</string> + <string name="width" msgid="9215847239714321097">"רוחב"</string> + <string name="height" msgid="3648885449443787772">"גובה"</string> + <string name="orientation" msgid="4958327983165245513">"כיוון"</string> + <string name="duration" msgid="8160058911218541616">"משך זמן"</string> + <string name="mimetype" msgid="3518268469266183548">"סוג MIME"</string> + <string name="file_size" msgid="4670384449129762138">"גודל קובץ"</string> + <string name="maker" msgid="7921835498034236197">"יוצר"</string> + <string name="model" msgid="8240207064064337366">"דגם"</string> + <string name="flash" msgid="2816779031261147723">"הבזק"</string> + <string name="aperture" msgid="5920657630303915195">"צמצם"</string> + <string name="focal_length" msgid="1291383769749877010">"רוחק מוקד"</string> + <string name="white_balance" msgid="8122534414851280901">"איזון לבן"</string> + <string name="exposure_time" msgid="3146642210127439553">"זמן חשיפה"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"מ\"מ"</string> + <string name="manual" msgid="6608905477477607865">"ידני"</string> + <string name="auto" msgid="4296941368722892821">"אוטומטי"</string> + <string name="flash_on" msgid="7891556231891837284">"צילום עם הבזק"</string> + <string name="flash_off" msgid="1445443413822680010">"ללא הבזק"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"הופך את האלבום לזמין באופן לא מקוון"</item> + <item quantity="other" msgid="6929905722448632886">"הופך את האלבומים לזמינים באופן לא מקוון"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"פריט זה מאוחסן באופן מקומי וזמין במצב לא מקוון."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"כל האלבומים"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"אלבומים מקומיים"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"התקני MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"אלבומי Picasa"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> של שטח פנוי"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> או פחות"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> או יותר"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> עד <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"ייבא"</string> + <string name="import_complete" msgid="1098450310074640619">"היבוא הושלם"</string> + <string name="import_fail" msgid="5205927625132482529">"היבוא נכשל"</string> + <string name="camera_connected" msgid="6984353643349303075">"המצלמה מחוברת"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"המצלמה מנותקת"</string> + <string name="click_import" msgid="6407959065464291972">"גע כאן כדי לייבא"</string> + <string name="widget_type_album" msgid="3245149644830731121">"תמונות מאלבום"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"ערבב את כל התמונות"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"בחר תמונה"</string> + <string name="widget_type" msgid="7308564524449340985">"סוג Widget"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"מצגת שקופיות"</string> + <string name="cache_status_title" msgid="8414708919928621485">"אחזור מראש של תמונות Picasa:"</string> + <string name="cache_status" msgid="7690438435538533106">"הורד <xliff:g id="NUMBER_0">%1$s</xliff:g> מתוך <xliff:g id="NUMBER_1">%2$s</xliff:g> תמונות"</string> + <string name="cache_done" msgid="9194449192869777483">"ההורדה הושלמה"</string> + <string name="albums" msgid="7320787705180057947">"אלבומים"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"מיקומים"</string> + <string name="people" msgid="4114003823747292747">"אנשים"</string> + <string name="tags" msgid="5539648765482935955">"תגיות"</string> + <string name="group_by" msgid="4308299657902209357">"קבץ לפי"</string> + <string name="settings" msgid="1534847740615665736">"הגדרות"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"הגדרות חשבון"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"הגדרות שימוש בנתונים"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"העלאה אוטומטית"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"הגדרות אחרות"</string> + <string name="about_gallery" msgid="8667445445883757255">"מידע על הגלריה"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"סנכרן ב-WiFi בלבד"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"העלה באופן אוטומטי את כל התמונות וסרטוני הווידאו שאתה מעביר לאלבום Google פרטי"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"אפשר העלאה אוטומטית"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"סינכרון תמונות של Google מופעל"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"סינכרון תמונות של Google כבוי"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"שנה העדפות סינכרון או הסר חשבון זה"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"הצג תמונות וסרטוני וידאו מחשבון זה בגלריה"</string> + <string name="add_account" msgid="4271217504968243974">"הוסף חשבון"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"בחר חשבון להעלאה אוטומטית"</string> +</resources> diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml new file mode 100644 index 000000000..d81560268 --- /dev/null +++ b/res/values-ja/strings.xml @@ -0,0 +1,172 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"ギャラリー"</string> + <string name="gadget_title" msgid="259405922673466798">"写真フレーム"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"動画プレーヤー"</string> + <string name="loading_video" msgid="4013492720121891585">"動画を読み込み中..."</string> + <string name="loading_image" msgid="1200894415793838191">"画像を読み込み中..."</string> + <string name="loading_account" msgid="928195413034552034">"アカウントを読み込み中..."</string> + <string name="resume_playing_title" msgid="8996677350649355013">"動画の再開"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"再生を%sから再開しますか?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"再生を再開"</string> + <string name="loading" msgid="7038208555304563571">"読み込み中..."</string> + <string name="fail_to_load" msgid="2710120770735315683">"読み込めませんでした"</string> + <string name="no_thumbnail" msgid="284723185546429750">"サムネイルなし"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"最初から再生"</string> + <string name="crop_save_text" msgid="8821167985419282305">"OK"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"顔をタップして開始します。"</string> + <string name="saving_image" msgid="7270334453636349407">"写真を保存中…"</string> + <string name="crop_label" msgid="521114301871349328">"トリミング"</string> + <string name="select_image" msgid="7841406150484742140">"写真を選択"</string> + <string name="select_video" msgid="4859510992798615076">"動画を選択"</string> + <string name="select_item" msgid="2257529413100472599">"アイテムを選択"</string> + <string name="select_album" msgid="4632641262236697235">"アルバムを選択"</string> + <string name="select_group" msgid="9090385962030340391">"グループを選択"</string> + <string name="set_image" msgid="2331476809308010401">"写真を設定:"</string> + <string name="wallpaper" msgid="9222901738515471972">"壁紙を設定しています。しばらくお待ちください..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"壁紙"</string> + <string name="delete" msgid="2839695998251824487">"削除"</string> + <string name="confirm_delete" msgid="5731757674837098707">"削除"</string> + <string name="cancel" msgid="3637516880917356226">"キャンセル"</string> + <string name="share" msgid="3619042788254195341">"共有"</string> + <string name="select_all" msgid="8623593677101437957">"すべて選択"</string> + <string name="deselect_all" msgid="7397531298370285581">"選択をすべて解除"</string> + <string name="slideshow" msgid="4355906903247112975">"スライドショー"</string> + <string name="details" msgid="8415120088556445230">"詳細情報"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"カメラに切り替え"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"地図に表示"</string> + <string name="rotate_left" msgid="7412075232752726934">"左に回転"</string> + <string name="rotate_right" msgid="7340681085011826618">"右に回転"</string> + <string name="no_such_item" msgid="3161074758669642065">"項目が見つかりません"</string> + <string name="edit" msgid="1502273844748580847">"編集"</string> + <string name="activity_not_found" msgid="3731390759313019518">"使用できるアプリケーションがありません"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"キャッシュリクエストの処理"</string> + <string name="caching_label" msgid="3244800874547101776">"キャッシュ中..."</string> + <string name="crop" msgid="7970750655414797277">"トリミング"</string> + <string name="set_as" msgid="3636764710790507868">"登録"</string> + <string name="video_err" msgid="7917736494827857757">"動画を再生できません"</string> + <string name="group_by_location" msgid="316641628989023253">"地域別"</string> + <string name="group_by_time" msgid="9046168567717963573">"時間別"</string> + <string name="group_by_tags" msgid="3568731317210676160">"タグ別"</string> + <string name="group_by_faces" msgid="1566351636227274906">"人物別"</string> + <string name="group_by_album" msgid="1532818636053818958">"アルバム別"</string> + <string name="group_by_size" msgid="153766174950394155">"サイズ別"</string> + <string name="untagged" msgid="7281481064509590402">"タグなし"</string> + <string name="no_location" msgid="2036710947563713111">"位置情報なし"</string> + <string name="show_images_only" msgid="7263218480867672653">"画像のみ"</string> + <string name="show_videos_only" msgid="3850394623678871697">"動画のみ"</string> + <string name="show_all" msgid="4780647751652596980">"画像と動画"</string> + <string name="appwidget_title" msgid="6410561146863700411">"フォトギャラリー"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"画像がありません"</string> + <string name="crop_saved" msgid="4684933379430649946">"トリミングした画像を[ダウンロード]に保存しました"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"トリミングした画像は保存されていません"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"使用できるアルバムはありません"</string> + <string name="empty_album" msgid="6307897398825514762">"使用できる画像/動画はありません"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasaウェブアルバム"</string> + <string name="picasa_posts" msgid="1055151689217481993">"バズ"</string> + <string name="make_available_offline" msgid="5157950985488297112">"オフラインで使用する"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"完了"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d/%2$d件:"</string> + <string name="title" msgid="7622928349908052569">"タイトル"</string> + <string name="description" msgid="3016729318096557520">"説明"</string> + <string name="time" msgid="1367953006052876956">"時刻"</string> + <string name="location" msgid="3432705876921618314">"場所"</string> + <string name="path" msgid="4725740395885105824">"パス"</string> + <string name="width" msgid="9215847239714321097">"幅"</string> + <string name="height" msgid="3648885449443787772">"高さ"</string> + <string name="orientation" msgid="4958327983165245513">"画面の向き"</string> + <string name="duration" msgid="8160058911218541616">"長さ"</string> + <string name="mimetype" msgid="3518268469266183548">"MIMEタイプ"</string> + <string name="file_size" msgid="4670384449129762138">"ファイルサイズ"</string> + <string name="maker" msgid="7921835498034236197">"作成者"</string> + <string name="model" msgid="8240207064064337366">"モデル"</string> + <string name="flash" msgid="2816779031261147723">"フラッシュ"</string> + <string name="aperture" msgid="5920657630303915195">"絞り"</string> + <string name="focal_length" msgid="1291383769749877010">"レンズ焦点距離"</string> + <string name="white_balance" msgid="8122534414851280901">"ホワイトバランス"</string> + <string name="exposure_time" msgid="3146642210127439553">"露出時間"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"マニュアル"</string> + <string name="auto" msgid="4296941368722892821">"オート"</string> + <string name="flash_on" msgid="7891556231891837284">"フラッシュON"</string> + <string name="flash_off" msgid="1445443413822680010">"フラッシュOFF"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"アルバムをオフラインで利用できるようにしています"</item> + <item quantity="other" msgid="6929905722448632886">"アルバムをオフラインで利用できるようにしています"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"このアイテムは端末に保存され、オフラインで利用できます。"</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"すべてのアルバム"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"ローカルアルバム"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"MTPデバイス"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Picasaのアルバム"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g>空き"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g>以下"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g>以上"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g>~<xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"インポート"</string> + <string name="import_complete" msgid="1098450310074640619">"インポート完了"</string> + <string name="import_fail" msgid="5205927625132482529">"インポートエラー"</string> + <string name="camera_connected" msgid="6984353643349303075">"カメラが接続されました"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"カメラが切断されました"</string> + <string name="click_import" msgid="6407959065464291972">"インポートするにはここをタップします"</string> + <string name="widget_type_album" msgid="3245149644830731121">"アルバムの画像"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"すべての画像をシャッフル"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"画像を選択"</string> + <string name="widget_type" msgid="7308564524449340985">"ウィジェットタイプ"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"スライドショー"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Picasaの画像をプリフェッチ:"</string> + <string name="cache_status" msgid="7690438435538533106">"<xliff:g id="NUMBER_0">%1$s</xliff:g>/<xliff:g id="NUMBER_1">%2$s</xliff:g>件の画像をダウンロード"</string> + <string name="cache_done" msgid="9194449192869777483">"ダウンロード完了"</string> + <string name="albums" msgid="7320787705180057947">"アルバム"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"ロケーション"</string> + <string name="people" msgid="4114003823747292747">"人物"</string> + <string name="tags" msgid="5539648765482935955">"タグ"</string> + <string name="group_by" msgid="4308299657902209357">"グループ化"</string> + <string name="settings" msgid="1534847740615665736">"設定"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"アカウント設定"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"データ使用設定"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"自動アップロード"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"その他の設定"</string> + <string name="about_gallery" msgid="8667445445883757255">"ギャラリーについて"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Wi-Fiでのみ同期"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"撮影したすべての画像と動画を限定公開のPicasaウェブアルバムに自動的にアップロードします"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"自動アップロードを有効にする"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Googleフォトの同期はONです"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Googleフォトの同期はOFFです"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"同期設定を変更またはこのアカウントを削除"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"このアカウントの画像と動画をギャラリーで表示"</string> + <string name="add_account" msgid="4271217504968243974">"アカウントを追加"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"自動アップロードするアカウント"</string> +</resources> diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml new file mode 100644 index 000000000..813f2968c --- /dev/null +++ b/res/values-ko/strings.xml @@ -0,0 +1,177 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"갤러리"</string> + <string name="gadget_title" msgid="259405922673466798">"사진 액자"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"동영상 플레이어"</string> + <string name="loading_video" msgid="4013492720121891585">"동영상 로드 중..."</string> + <string name="loading_image" msgid="1200894415793838191">"이미지 로드 중…"</string> + <string name="loading_account" msgid="928195413034552034">"계정 로드 중..."</string> + <string name="resume_playing_title" msgid="8996677350649355013">"동영상 다시 시작"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"%s부터 이어서 보시겠습니까?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"이어서 보기"</string> + <string name="loading" msgid="7038208555304563571">"로드 중..."</string> + <string name="fail_to_load" msgid="2710120770735315683">"로드 실패"</string> + <string name="no_thumbnail" msgid="284723185546429750">"미리보기 이미지 없음"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"처음부터 보기"</string> + <string name="crop_save_text" msgid="8821167985419282305">"확인"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"시작하려면 얼굴을 탭하세요."</string> + <string name="saving_image" msgid="7270334453636349407">"사진 저장 중..."</string> + <string name="crop_label" msgid="521114301871349328">"사진 자르기"</string> + <string name="select_image" msgid="7841406150484742140">"사진 선택"</string> + <string name="select_video" msgid="4859510992798615076">"동영상 선택"</string> + <string name="select_item" msgid="2257529413100472599">"항목 선택"</string> + <string name="select_album" msgid="4632641262236697235">"앨범 선택"</string> + <string name="select_group" msgid="9090385962030340391">"그룹 선택"</string> + <string name="set_image" msgid="2331476809308010401">"사진을 다음으로 설정"</string> + <string name="wallpaper" msgid="9222901738515471972">"배경화면을 설정하는 중입니다. 잠시 기다려 주세요..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"배경화면"</string> + <string name="delete" msgid="2839695998251824487">"삭제"</string> + <string name="confirm_delete" msgid="5731757674837098707">"삭제 확인"</string> + <string name="cancel" msgid="3637516880917356226">"취소"</string> + <string name="share" msgid="3619042788254195341">"공유"</string> + <string name="select_all" msgid="8623593677101437957">"모두 선택"</string> + <string name="deselect_all" msgid="7397531298370285581">"모두 선택취소"</string> + <string name="slideshow" msgid="4355906903247112975">"슬라이드쇼"</string> + <string name="details" msgid="8415120088556445230">"세부정보"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"카메라로 전환"</string> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"%1$d개 선택됨"</item> + <item quantity="one" msgid="2478365152745637768">"%1$d개 선택됨"</item> + <item quantity="other" msgid="754722656147810487">"%1$d개 선택됨"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"%1$d개 선택됨"</item> + <item quantity="one" msgid="6184377003099987825">"%1$d개 선택됨"</item> + <item quantity="other" msgid="53105607141906130">"%1$d개 선택됨"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"%1$d개 선택됨"</item> + <item quantity="one" msgid="5030162638216034260">"%1$d개 선택됨"</item> + <item quantity="other" msgid="3512041363942842738">"%1$d개 선택됨"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"지도에 표시"</string> + <string name="rotate_left" msgid="7412075232752726934">"왼쪽으로 회전"</string> + <string name="rotate_right" msgid="7340681085011826618">"오른쪽으로 회전"</string> + <string name="no_such_item" msgid="3161074758669642065">"항목을 찾을 수 없습니다."</string> + <string name="edit" msgid="1502273844748580847">"수정"</string> + <string name="activity_not_found" msgid="3731390759313019518">"사용할 수 있는 애플리케이션이 없습니다."</string> + <string name="process_caching_requests" msgid="1076938190997999614">"캐시 요청 처리"</string> + <string name="caching_label" msgid="3244800874547101776">"캐시 중..."</string> + <string name="crop" msgid="7970750655414797277">"자르기"</string> + <string name="set_as" msgid="3636764710790507868">"다음으로 설정"</string> + <string name="video_err" msgid="7917736494827857757">"동영상을 재생할 수 없음"</string> + <string name="group_by_location" msgid="316641628989023253">"위치별"</string> + <string name="group_by_time" msgid="9046168567717963573">"시간별"</string> + <string name="group_by_tags" msgid="3568731317210676160">"태그별"</string> + <string name="group_by_faces" msgid="1566351636227274906">"인물 기준"</string> + <string name="group_by_album" msgid="1532818636053818958">"앨범별"</string> + <string name="group_by_size" msgid="153766174950394155">"크기별"</string> + <string name="untagged" msgid="7281481064509590402">"태그 지정 안함"</string> + <string name="no_location" msgid="2036710947563713111">"위치 정보 없음"</string> + <string name="show_images_only" msgid="7263218480867672653">"이미지"</string> + <string name="show_videos_only" msgid="3850394623678871697">"동영상"</string> + <string name="show_all" msgid="4780647751652596980">"이미지 및 동영상"</string> + <string name="appwidget_title" msgid="6410561146863700411">"사진 갤러리"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"사진 없음"</string> + <string name="crop_saved" msgid="4684933379430649946">"잘린 이미지가 다운로드 폴더에 저장되었습니다."</string> + <string name="crop_not_saved" msgid="1438309290700431923">"잘린 이미지가 저장되지 않았습니다."</string> + <string name="no_albums_alert" msgid="3459550423604532470">"사용할 수 있는 앨범이 없습니다"</string> + <string name="empty_album" msgid="6307897398825514762">"사용할 수 있는 이미지/동영상이 없습니다."</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa 웹앨범"</string> + <string name="picasa_posts" msgid="1055151689217481993">"버즈"</string> + <string name="make_available_offline" msgid="5157950985488297112">"오프라인 사용 설정"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"완료"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%2$d개 중 %1$d번째 항목:"</string> + <string name="title" msgid="7622928349908052569">"제목"</string> + <string name="description" msgid="3016729318096557520">"설명"</string> + <string name="time" msgid="1367953006052876956">"시간"</string> + <string name="location" msgid="3432705876921618314">"위치"</string> + <string name="path" msgid="4725740395885105824">"경로"</string> + <string name="width" msgid="9215847239714321097">"너비"</string> + <string name="height" msgid="3648885449443787772">"높이"</string> + <string name="orientation" msgid="4958327983165245513">"방향"</string> + <string name="duration" msgid="8160058911218541616">"길이"</string> + <string name="mimetype" msgid="3518268469266183548">"MIME 형식"</string> + <string name="file_size" msgid="4670384449129762138">"파일 크기"</string> + <string name="maker" msgid="7921835498034236197">"제조업체"</string> + <string name="model" msgid="8240207064064337366">"모델"</string> + <string name="flash" msgid="2816779031261147723">"플래시"</string> + <string name="aperture" msgid="5920657630303915195">"조리개"</string> + <string name="focal_length" msgid="1291383769749877010">"초점 거리"</string> + <string name="white_balance" msgid="8122534414851280901">"화이트 밸런스"</string> + <string name="exposure_time" msgid="3146642210127439553">"노출 시간"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"수동"</string> + <string name="auto" msgid="4296941368722892821">"자동"</string> + <string name="flash_on" msgid="7891556231891837284">"플래시 터짐"</string> + <string name="flash_off" msgid="1445443413822680010">"플래시 없음"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"오프라인에서 앨범 사용 가능"</item> + <item quantity="other" msgid="6929905722448632886">"오프라인에서 앨범 사용 가능"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"이 항목은 로컬에 저장되어 있으며 오프라인에서 사용할 수 있습니다."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"모든 앨범"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"로컬 앨범"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"MTP 기기"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Picasa 앨범"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> 사용 가능"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> 이하"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> 이상"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> - <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"가져오기"</string> + <string name="import_complete" msgid="1098450310074640619">"가져오기 완료"</string> + <string name="import_fail" msgid="5205927625132482529">"가져오기 실패"</string> + <string name="camera_connected" msgid="6984353643349303075">"카메라 연결됨"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"카메라 연결이 해제됨"</string> + <string name="click_import" msgid="6407959065464291972">"가져오려면 여기를 터치하세요."</string> + <string name="widget_type_album" msgid="3245149644830731121">"앨범의 이미지"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"모든 이미지 셔플"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"이미지 선택"</string> + <string name="widget_type" msgid="7308564524449340985">"위젯 유형"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"슬라이드쇼"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Picasa 사진 미리 가져오기:"</string> + <string name="cache_status" msgid="7690438435538533106">"사진 <xliff:g id="NUMBER_1">%2$s</xliff:g>개 중 <xliff:g id="NUMBER_0">%1$s</xliff:g>개 다운로드"</string> + <string name="cache_done" msgid="9194449192869777483">"다운로드 완료"</string> + <string name="albums" msgid="7320787705180057947">"앨범"</string> + <string name="times" msgid="2023033894889499219">"횟수"</string> + <string name="locations" msgid="6649297994083130305">"위치"</string> + <string name="people" msgid="4114003823747292747">"사용자"</string> + <string name="tags" msgid="5539648765482935955">"태그"</string> + <string name="group_by" msgid="4308299657902209357">"그룹화 기준"</string> + <string name="settings" msgid="1534847740615665736">"설정"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"계정 설정"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"데이터 사용 설정"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"자동 업로드"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"기타 설정"</string> + <string name="about_gallery" msgid="8667445445883757255">"갤러리 정보"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"WiFi에서만 동기화"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"촬영한 모든 사진과 동영상을 비공개 Picasa 웹앨범에 자동으로 업로드합니다."</string> + <string name="enable_auto_upload" msgid="1586329406342131">"자동 업로드 사용"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Google 포토 동기화 사용 중"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Google 사진 동기화 사용 안함"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"동기화 맞춤설정을 변경하거나 계정을 삭제합니다."</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"갤러리에서 이 계정의 사진과 동영상 보기"</string> + <string name="add_account" msgid="4271217504968243974">"계정 추가"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"자동 업로드 계정 선택"</string> +</resources> diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml new file mode 100644 index 000000000..43e9726ae --- /dev/null +++ b/res/values-lt/strings.xml @@ -0,0 +1,173 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galerija"</string> + <string name="gadget_title" msgid="259405922673466798">"Paveikslėlio rėmelis"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Vaizdo įrašų grotuvas"</string> + <string name="loading_video" msgid="4013492720121891585">"Įkeliamas vaizdo įrašas..."</string> + <string name="loading_image" msgid="1200894415793838191">"Įkeliamas vaizdas..."</string> + <string name="loading_account" msgid="928195413034552034">"Įkeliama paskyra???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Atnaujinti vaizdo įrašą"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Atnaujinti leidimą nuo %s?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Atnaujinti grojimą"</string> + <string name="loading" msgid="7038208555304563571">"Įkeliama…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"Nepavyko įkelti"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Nėra miniatiūros"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Pradėti iš naujo"</string> + <string name="crop_save_text" msgid="8821167985419282305">"Gerai"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Bakstelėkite veidą, jei norite pradėti."</string> + <string name="saving_image" msgid="7270334453636349407">"Išsaugomas paveikslėlis..."</string> + <string name="crop_label" msgid="521114301871349328">"Apkarpyti paveikslėlį"</string> + <string name="select_image" msgid="7841406150484742140">"Pasirinkti nuotrauką"</string> + <string name="select_video" msgid="4859510992798615076">"Pasir. vaizdo įrašą"</string> + <string name="select_item" msgid="2257529413100472599">"Pasir. elem."</string> + <string name="select_album" msgid="4632641262236697235">"Pasir. alb."</string> + <string name="select_group" msgid="9090385962030340391">"Pasir. gr."</string> + <string name="set_image" msgid="2331476809308010401">"Nustatyti paveikslėlį kaip"</string> + <string name="wallpaper" msgid="9222901738515471972">"Nustatomas darbalaukio fonas, palaukite..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Darbalaukio fonas"</string> + <string name="delete" msgid="2839695998251824487">"Ištrinti"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Patvirtinti ištrynimą"</string> + <string name="cancel" msgid="3637516880917356226">"Atšaukti"</string> + <string name="share" msgid="3619042788254195341">"Bendrinti"</string> + <string name="select_all" msgid="8623593677101437957">"Pasirinkti viską"</string> + <string name="deselect_all" msgid="7397531298370285581">"Panaikinti visus žymėjimus"</string> + <string name="slideshow" msgid="4355906903247112975">"Skaidrių demonstracija"</string> + <string name="details" msgid="8415120088556445230">"Išsami informacija"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Perjungti į fotoaparatą"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"Rodyti žemėlapyje"</string> + <string name="rotate_left" msgid="7412075232752726934">"Sukti į kairę"</string> + <string name="rotate_right" msgid="7340681085011826618">"Sukti į dešinę"</string> + <string name="no_such_item" msgid="3161074758669642065">"Elementas nerastas"</string> + <string name="edit" msgid="1502273844748580847">"Redaguoti"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Nėra pasiekiamų programų"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Apdoroti padėjimo užklausas"</string> + <string name="caching_label" msgid="3244800874547101776">"Padėjimas..."</string> + <string name="crop" msgid="7970750655414797277">"Apkarpyti"</string> + <string name="set_as" msgid="3636764710790507868">"Nustatyti kaip"</string> + <string name="video_err" msgid="7917736494827857757">"Neįmanoma paleisti vaizdo įrašo"</string> + <string name="group_by_location" msgid="316641628989023253">"Pagal vietą"</string> + <string name="group_by_time" msgid="9046168567717963573">"Pagal laiką"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Pagal žymas"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Pagal žmones"</string> + <string name="group_by_album" msgid="1532818636053818958">"Pagal albumą"</string> + <string name="group_by_size" msgid="153766174950394155">"Pagal dydį"</string> + <string name="untagged" msgid="7281481064509590402">"Nepažymėta"</string> + <string name="no_location" msgid="2036710947563713111">"Nėra vietos"</string> + <string name="show_images_only" msgid="7263218480867672653">"Tik vaizdai"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Tik vaizdo įrašai"</string> + <string name="show_all" msgid="4780647751652596980">"Vaizdai ir vaizdo įrašai"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Nuotraukų galerija"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Nėra nuotraukų"</string> + <string name="crop_saved" msgid="4684933379430649946">"Apkarpytas vaizdas išsaugotas atsisiuntimų aplanke"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Apkarpytas vaizdas neišsaugotas"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Nėra pasiekiamų albumų"</string> + <string name="empty_album" msgid="6307897398825514762">"Nėra pasiekiamų vaizdų / vaizdo įrašų"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"„Picasa“ žiniatinklio albumai"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Padaryti pasiekiamą neprisij."</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Atlikta"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d iš %2$d element.:"</string> + <string name="title" msgid="7622928349908052569">"Pareigos"</string> + <string name="description" msgid="3016729318096557520">"Apibūdinimas"</string> + <string name="time" msgid="1367953006052876956">"Laikas"</string> + <string name="location" msgid="3432705876921618314">"Vieta"</string> + <string name="path" msgid="4725740395885105824">"Kelias"</string> + <string name="width" msgid="9215847239714321097">"Plotis"</string> + <string name="height" msgid="3648885449443787772">"Aukštis"</string> + <string name="orientation" msgid="4958327983165245513">"Padėtis"</string> + <string name="duration" msgid="8160058911218541616">"Trukmė"</string> + <string name="mimetype" msgid="3518268469266183548">"MIME tipas"</string> + <string name="file_size" msgid="4670384449129762138">"Failo dydis"</string> + <string name="maker" msgid="7921835498034236197">"Kūrėjas"</string> + <string name="model" msgid="8240207064064337366">"Modelis"</string> + <string name="flash" msgid="2816779031261147723">"Blykstė"</string> + <string name="aperture" msgid="5920657630303915195">"Diafragma"</string> + <string name="focal_length" msgid="1291383769749877010">"Židinio nuotolis"</string> + <string name="white_balance" msgid="8122534414851280901">"Baltos spalvos balansas"</string> + <string name="exposure_time" msgid="3146642210127439553">"Išlaikymo laikas"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Rankiniu būdu"</string> + <string name="auto" msgid="4296941368722892821">"Automobiliai"</string> + <string name="flash_on" msgid="7891556231891837284">"Blykstė suveikė"</string> + <string name="flash_off" msgid="1445443413822680010">"Be blykstės"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Nustatoma, kad albumas būtų pasiekiamas neprisij."</item> + <item quantity="other" msgid="6929905722448632886">"Nustatoma, kad albumai būtų pasiekiami neprisij."</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Ši prekė saugoma vietinėje atmintinėje ir yra pasiekiama neprisijungus."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Visi albumai"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Vietiniai albumai"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"MTP įrenginiai"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"„Picasa“ albumai"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> laisvos vietos"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> ar mažiau"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> ar daugiau"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g>–<xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Importuoti"</string> + <string name="import_complete" msgid="1098450310074640619">"Importavimas baigtas"</string> + <string name="import_fail" msgid="5205927625132482529">"Įvyko importavimo klaida"</string> + <string name="camera_connected" msgid="6984353643349303075">"Fotoaparatas prijungtas"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Fotoaparatas atjungtas"</string> + <string name="click_import" msgid="6407959065464291972">"Jei norite importuoti, palieskite čia"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Vaizdai iš albumo"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Maišyti visus vaizdus"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Pasirinkite vaizdą"</string> + <string name="widget_type" msgid="7308564524449340985">"Valdiklio tipas"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Skaidrių demonstrac."</string> + <string name="cache_status_title" msgid="8414708919928621485">"Iš anksto pateikiamos „Picasa“ nuotr.:"</string> + <string name="cache_status" msgid="7690438435538533106">"<xliff:g id="NUMBER_0">%1$s</xliff:g> nuotr. iš <xliff:g id="NUMBER_1">%2$s</xliff:g> atsisiuntimas"</string> + <string name="cache_done" msgid="9194449192869777483">"Atsisiuntimas baigtas"</string> + <string name="albums" msgid="7320787705180057947">"Albumai"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"Vietos"</string> + <string name="people" msgid="4114003823747292747">"Žmonės"</string> + <string name="tags" msgid="5539648765482935955">"Žymos"</string> + <string name="group_by" msgid="4308299657902209357">"Grupuoti pagal"</string> + <!-- no translation found for settings (1534847740615665736) --> + <skip /> + <string name="prefs_accounts" msgid="7942761992713671670">"Paskyros nustatymai"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Duomenų naudojimo nustatymai"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Automatinis įkėlimas"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Kiti nustatymai"</string> + <string name="about_gallery" msgid="8667445445883757255">"Apie galeriją"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Sinchronizuoti tik naudojant „Wi-Fi“"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Automatiškai įkelti visas nuotraukas ir vaizdo įrašus, kuriuos kelsite į privatų „Picasa“ žiniatinklio albumą"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Įgalinti automatinį įkėlimą"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"„Google“ nuotrauk. sinchr. įj."</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"„Google“ nuotr. sinchron. išj."</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Keisti sin. nuost. arba pašal. šią pask."</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Žiūrėti nuotraukas ir vaizdo įrašus šios paskyros galerijoje"</string> + <string name="add_account" msgid="4271217504968243974">"Pridėti paskyrą"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Pasirinkti autom. įkel. pask."</string> +</resources> diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml new file mode 100644 index 000000000..2af5425ea --- /dev/null +++ b/res/values-lv/strings.xml @@ -0,0 +1,173 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galerija"</string> + <string name="gadget_title" msgid="259405922673466798">"Attēla ietvars"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Video atskaņotājs"</string> + <string name="loading_video" msgid="4013492720121891585">"Notiek video ielāde..."</string> + <string name="loading_image" msgid="1200894415793838191">"Notiek attēla ielāde…"</string> + <string name="loading_account" msgid="928195413034552034">"Notiek konta ielāde???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Atsākt video atskaņošanu"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Vai atsākt atskaņošanu no %s?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Atsākt atskaņošanu"</string> + <string name="loading" msgid="7038208555304563571">"Notiek ielāde…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"Neizdevās ielādēt"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Nav sīktēla."</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Sākt vēlreiz"</string> + <string name="crop_save_text" msgid="8821167985419282305">"Labi"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Pieskarieties sejai, lai sāktu."</string> + <string name="saving_image" msgid="7270334453636349407">"Notiek attēla saglabāšana..."</string> + <string name="crop_label" msgid="521114301871349328">"Apgriezt attēlu"</string> + <string name="select_image" msgid="7841406150484742140">"Atlasiet fotoattēlu"</string> + <string name="select_video" msgid="4859510992798615076">"Atlasiet videoklipu"</string> + <string name="select_item" msgid="2257529413100472599">"Atlas. vienumu(-us)"</string> + <string name="select_album" msgid="4632641262236697235">"Atlasiet albumu(-us)"</string> + <string name="select_group" msgid="9090385962030340391">"Atlasiet grupu(-as)"</string> + <string name="set_image" msgid="2331476809308010401">"Iestatīt attēlu kā:"</string> + <string name="wallpaper" msgid="9222901738515471972">"Notiek tapetes iestatīšana, lūdzu, uzgaidiet..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Fona tapete"</string> + <string name="delete" msgid="2839695998251824487">"Dzēst"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Apstiprināt dzēšanu"</string> + <string name="cancel" msgid="3637516880917356226">"Atcelt"</string> + <string name="share" msgid="3619042788254195341">"Dalies"</string> + <string name="select_all" msgid="8623593677101437957">"Atlasīt visu"</string> + <string name="deselect_all" msgid="7397531298370285581">"Noņemt visas atzīmes"</string> + <string name="slideshow" msgid="4355906903247112975">"Slaidrāde"</string> + <string name="details" msgid="8415120088556445230">"Detalizēta informācija"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Pārslēgšanās uz liet. Kamera"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"Rādīt kartē"</string> + <string name="rotate_left" msgid="7412075232752726934">"Pagriezt pa kreisi"</string> + <string name="rotate_right" msgid="7340681085011826618">"Pagriezt pa labi"</string> + <string name="no_such_item" msgid="3161074758669642065">"Vienums nav atrasts."</string> + <string name="edit" msgid="1502273844748580847">"Rediģēt"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Nav pieejama neviena lietojumprogramma"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Kešdarbes pieprasījumu apstrāde"</string> + <string name="caching_label" msgid="3244800874547101776">"Notiek kešdarbe..."</string> + <string name="crop" msgid="7970750655414797277">"Apgriezt"</string> + <string name="set_as" msgid="3636764710790507868">"Iestatīt kā:"</string> + <string name="video_err" msgid="7917736494827857757">"Nevar atskaņot video."</string> + <string name="group_by_location" msgid="316641628989023253">"Pēc atrašanās vietas"</string> + <string name="group_by_time" msgid="9046168567717963573">"Pēc uzņemšanas laika"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Pēc atzīmēm"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Pēc personām"</string> + <string name="group_by_album" msgid="1532818636053818958">"Pēc albumiem"</string> + <string name="group_by_size" msgid="153766174950394155">"Pēc izmēra"</string> + <string name="untagged" msgid="7281481064509590402">"Bez atzīmēm"</string> + <string name="no_location" msgid="2036710947563713111">"Nav vietas inform."</string> + <string name="show_images_only" msgid="7263218480867672653">"Tikai attēli"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Tikai videoklipi"</string> + <string name="show_all" msgid="4780647751652596980">"Attēli un videoklipi"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Fotogalerija"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Nav fotoattēlu"</string> + <string name="crop_saved" msgid="4684933379430649946">"Apgrieztais attēls saglabāts lejupielādes laikā."</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Apgrieztais attēls nav saglabāts."</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Nav pieejams neviens albums"</string> + <string name="empty_album" msgid="6307897398825514762">"Nav pieejams neviens attēls/videoklips"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa tīmekļa albumi"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Padarīt pieejamu bezsaistē"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Gatavs"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d no %2$d vienumiem:"</string> + <string name="title" msgid="7622928349908052569">"Nosaukums"</string> + <string name="description" msgid="3016729318096557520">"Apraksts"</string> + <string name="time" msgid="1367953006052876956">"Laiks"</string> + <string name="location" msgid="3432705876921618314">"Atrašanās vieta"</string> + <string name="path" msgid="4725740395885105824">"Ceļš"</string> + <string name="width" msgid="9215847239714321097">"Platums"</string> + <string name="height" msgid="3648885449443787772">"Augstums"</string> + <string name="orientation" msgid="4958327983165245513">"Orientācija"</string> + <string name="duration" msgid="8160058911218541616">"Ilgums"</string> + <string name="mimetype" msgid="3518268469266183548">"MIME tips"</string> + <string name="file_size" msgid="4670384449129762138">"Faila lielums"</string> + <string name="maker" msgid="7921835498034236197">"Izgatavotājs"</string> + <string name="model" msgid="8240207064064337366">"Modelis"</string> + <string name="flash" msgid="2816779031261147723">"Zibspuldze"</string> + <string name="aperture" msgid="5920657630303915195">"Diafragmas atvērums"</string> + <string name="focal_length" msgid="1291383769749877010">"Fokusa attālums"</string> + <string name="white_balance" msgid="8122534414851280901">"Baltās krāsas balanss"</string> + <string name="exposure_time" msgid="3146642210127439553">"Ekspozīcijas ilgums"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Rokasgrāmata"</string> + <string name="auto" msgid="4296941368722892821">"Automātiski"</string> + <string name="flash_on" msgid="7891556231891837284">"Zibspuldze ir aktivizēta"</string> + <string name="flash_off" msgid="1445443413822680010">"Bez zibspuldzes"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Albums tiek padarīts pieejams bezsaistē."</item> + <item quantity="other" msgid="6929905722448632886">"Albumi tiek padarīti pieejami bezsaistē."</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Šis vienums tiek glabāts lokāli un ir pieejams bezsaistē."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Visi albumi"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Albumi ierīcē"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"MTP ierīces"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Picasa albumi"</string> + <string name="free_space_format" msgid="8766337315709161215">"Brīva vieta: <xliff:g id="BYTES">%s</xliff:g>"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> vai mazāk"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> vai vairāk"</string> + <string name="size_between" msgid="8779660840898917208">"No <xliff:g id="MIN_SIZE">%1$s</xliff:g> līdz <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Importēt"</string> + <string name="import_complete" msgid="1098450310074640619">"Import. pabeigta."</string> + <string name="import_fail" msgid="5205927625132482529">"Importēšana neizdevās."</string> + <string name="camera_connected" msgid="6984353643349303075">"Kamera ir pievienota."</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Kamera ir atvienota."</string> + <string name="click_import" msgid="6407959065464291972">"Pieskarieties šeit, lai importētu."</string> + <string name="widget_type_album" msgid="3245149644830731121">"Attēli no albuma"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Rādīt attēlus jauktā secībā"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Izvēlēties attēlu"</string> + <string name="widget_type" msgid="7308564524449340985">"Logrīka veids"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Slaidrāde"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Notiek Picasa fotoattēlu priekšienese"</string> + <string name="cache_status" msgid="7690438435538533106">"Lejupielād. <xliff:g id="NUMBER_0">%1$s</xliff:g> fotoattēls(-i) no <xliff:g id="NUMBER_1">%2$s</xliff:g>"</string> + <string name="cache_done" msgid="9194449192869777483">"Lejupielāde pabeigta"</string> + <string name="albums" msgid="7320787705180057947">"Albumi"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"Vietas"</string> + <string name="people" msgid="4114003823747292747">"Personas"</string> + <string name="tags" msgid="5539648765482935955">"Atzīmes"</string> + <string name="group_by" msgid="4308299657902209357">"Grupēt pēc:"</string> + <!-- no translation found for settings (1534847740615665736) --> + <skip /> + <string name="prefs_accounts" msgid="7942761992713671670">"Konta iestatījumi"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Datu izmantošanas iestatījumi"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Automātiskā augšupielāde"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Citi iestatījumi"</string> + <string name="about_gallery" msgid="8667445445883757255">"Par galeriju"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Sinhronizēt tikai Wi-Fi tīklā"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Automātiski augšupielādējiet visus uzņemtos fotoattēlus un videoklipus privātā Picasa tīmekļa albumā."</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Automātiskās augšupielādes iespējošana"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Google fotoatt. sinhr. iesl."</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Google fotoatt. sinhr. izsl."</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Mainiet sinhr. pref. vai noņ. šo kontu."</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Skatīt šī konta fotoattēlus un videoklipus galerijā"</string> + <string name="add_account" msgid="4271217504968243974">"Konta pievienošana"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Autom. augšupiel. konta izvēle"</string> +</resources> diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml new file mode 100644 index 000000000..7aeb5a1ea --- /dev/null +++ b/res/values-ms/strings.xml @@ -0,0 +1,172 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galeri"</string> + <string name="gadget_title" msgid="259405922673466798">"Bingkai gambar"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Pemain video"</string> + <string name="loading_video" msgid="4013492720121891585">"Memuatkan video..."</string> + <string name="loading_image" msgid="1200894415793838191">"Memuatkan imej..."</string> + <string name="loading_account" msgid="928195413034552034">"Memuatkan akaun???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Sambung semula video"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Sambung semula proses main dari %s ?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Sambung semula proses main"</string> + <string name="loading" msgid="7038208555304563571">"Memuatkan..."</string> + <string name="fail_to_load" msgid="2710120770735315683">"Gagal dimuatkan"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Tiada lakaran kenit"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Mainkan semula dari mula"</string> + <string name="crop_save_text" msgid="8821167985419282305">"Ok"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Ketik wajah untuk memulakan."</string> + <string name="saving_image" msgid="7270334453636349407">"Menyimpan gambar..."</string> + <string name="crop_label" msgid="521114301871349328">"Pangkas gambar"</string> + <string name="select_image" msgid="7841406150484742140">"Pilih foto"</string> + <string name="select_video" msgid="4859510992798615076">"Pilih video"</string> + <string name="select_item" msgid="2257529413100472599">"Pilih item"</string> + <string name="select_album" msgid="4632641262236697235">"Pilih album"</string> + <string name="select_group" msgid="9090385962030340391">"Pilih kumpulan"</string> + <string name="set_image" msgid="2331476809308010401">"Tetapkan gambar sebagai"</string> + <string name="wallpaper" msgid="9222901738515471972">"Menetapkan kertas dinding, sila tunggu..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Kertas dinding"</string> + <string name="delete" msgid="2839695998251824487">"Padam"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Sahkan Pemadaman"</string> + <string name="cancel" msgid="3637516880917356226">"Batal"</string> + <string name="share" msgid="3619042788254195341">"Kongsi"</string> + <string name="select_all" msgid="8623593677101437957">"Pilih Semua"</string> + <string name="deselect_all" msgid="7397531298370285581">"Nyahpilih Semua"</string> + <string name="slideshow" msgid="4355906903247112975">"Tayangan slaid"</string> + <string name="details" msgid="8415120088556445230">"Butiran"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Bertukar kepada kamera"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"Tunjukkan pada peta"</string> + <string name="rotate_left" msgid="7412075232752726934">"Putar Kiri"</string> + <string name="rotate_right" msgid="7340681085011826618">"Putar Kanan"</string> + <string name="no_such_item" msgid="3161074758669642065">"Item tidak ditemui"</string> + <string name="edit" msgid="1502273844748580847">"Edit"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Tiada aplikasi tersedia"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Memproses Permintaan Cache"</string> + <string name="caching_label" msgid="3244800874547101776">"Mengcache..."</string> + <string name="crop" msgid="7970750655414797277">"Pangkas"</string> + <string name="set_as" msgid="3636764710790507868">"Tetapkan sebagai"</string> + <string name="video_err" msgid="7917736494827857757">"Tidak boleh memainkan video"</string> + <string name="group_by_location" msgid="316641628989023253">"Mengikut lokasi"</string> + <string name="group_by_time" msgid="9046168567717963573">"Mengikut masa"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Mengikut teg"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Mengikut orang"</string> + <string name="group_by_album" msgid="1532818636053818958">"Mengikut album"</string> + <string name="group_by_size" msgid="153766174950394155">"Mengikut saiz"</string> + <string name="untagged" msgid="7281481064509590402">"Tidak ditanda namakan"</string> + <string name="no_location" msgid="2036710947563713111">"Tiada Lokasi"</string> + <string name="show_images_only" msgid="7263218480867672653">"Imej sahaja"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Video sahaja"</string> + <string name="show_all" msgid="4780647751652596980">"Imej dan video"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Galeri Foto"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Tiada Foto"</string> + <string name="crop_saved" msgid="4684933379430649946">"Imej yg dipangkas telah disimpan dalam muat turun"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Imej yang dipangkas belum disimpan"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Tiada album tersedia"</string> + <string name="empty_album" msgid="6307897398825514762">"Tiada imej/video tersedia"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Album Web Picasa"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Jadikan tersedia luar talian"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Selesai"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d dari %2$d item:"</string> + <string name="title" msgid="7622928349908052569">"Tajuk"</string> + <string name="description" msgid="3016729318096557520">"Perihalan"</string> + <string name="time" msgid="1367953006052876956">"Masa"</string> + <string name="location" msgid="3432705876921618314">"Lokasi"</string> + <string name="path" msgid="4725740395885105824">"Laluan"</string> + <string name="width" msgid="9215847239714321097">"Lebar"</string> + <string name="height" msgid="3648885449443787772">"Tinggi"</string> + <string name="orientation" msgid="4958327983165245513">"Orientasi"</string> + <string name="duration" msgid="8160058911218541616">"Tempoh"</string> + <string name="mimetype" msgid="3518268469266183548">"Jenis MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Saiz Fail"</string> + <string name="maker" msgid="7921835498034236197">"Pembuat"</string> + <string name="model" msgid="8240207064064337366">"Model"</string> + <string name="flash" msgid="2816779031261147723">"Flash"</string> + <string name="aperture" msgid="5920657630303915195">"Bukaan"</string> + <string name="focal_length" msgid="1291383769749877010">"Jarak Fokus"</string> + <string name="white_balance" msgid="8122534414851280901">"Imbangan Putih"</string> + <string name="exposure_time" msgid="3146642210127439553">"Masa Dedahan"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Manual"</string> + <string name="auto" msgid="4296941368722892821">"Auto"</string> + <string name="flash_on" msgid="7891556231891837284">"Denyar dilepas"</string> + <string name="flash_off" msgid="1445443413822680010">"Tiada denyar"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Menjadikan album tersedia di luar talian"</item> + <item quantity="other" msgid="6929905722448632886">"Menjadikan album tersedia di luar talian"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Item ini disimpan pada peranti dan tersedia di luar talian."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Semua Album"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Album Setempat"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"Peranti MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Album Picasa"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> percuma"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> atau kurang"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> atau kurang"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> hingga <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Import"</string> + <string name="import_complete" msgid="1098450310074640619">"Import Selesai"</string> + <string name="import_fail" msgid="5205927625132482529">"Import Gagal"</string> + <string name="camera_connected" msgid="6984353643349303075">"Kamera disambungkan"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Kamera diputuskan sambungan"</string> + <string name="click_import" msgid="6407959065464291972">"Sentuh di sini untuk mengimport"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Imej dari album"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Rombak semua imej"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Pilih imej"</string> + <string name="widget_type" msgid="7308564524449340985">"Jenis Widget"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Tayangan slaid"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Pra-ambil foto picasa:"</string> + <string name="cache_status" msgid="7690438435538533106">"Muat turun <xliff:g id="NUMBER_0">%1$s</xliff:g> daripada <xliff:g id="NUMBER_1">%2$s</xliff:g> foto"</string> + <string name="cache_done" msgid="9194449192869777483">"Muat turun selesai"</string> + <string name="albums" msgid="7320787705180057947">"Album"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"Lokasi"</string> + <string name="people" msgid="4114003823747292747">"Orang"</string> + <string name="tags" msgid="5539648765482935955">"Teg"</string> + <string name="group_by" msgid="4308299657902209357">"Kumpulkan mengikut"</string> + <string name="settings" msgid="1534847740615665736">"Tetapan"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"Tetapan akaun"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Tetapan penggunaan data"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Automuat naik"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Tetapan lain"</string> + <string name="about_gallery" msgid="8667445445883757255">"Mengenai Galeri"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Segerak pada WiFi sahaja"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Muat naik semua foto dan video yang anda ambil ke album web picasa peribadi secara automatik"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Dayakan Automuat naik"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Pnygrkn foto Google DIHIDUPKAN"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Pnygrkn foto Google DIMATIKAN"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Tkr plhn sgrk atau alih keluar akaun ini"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"LIhat foto dan video daripada akaun ini dalam Galeri"</string> + <string name="add_account" msgid="4271217504968243974">"Tambah akaun"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Pilih akaun Automuat naik"</string> +</resources> diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml new file mode 100644 index 000000000..9512d44b0 --- /dev/null +++ b/res/values-nb/strings.xml @@ -0,0 +1,172 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galleri"</string> + <string name="gadget_title" msgid="259405922673466798">"Bilderamme"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Videospiller"</string> + <string name="loading_video" msgid="4013492720121891585">"Laster video…"</string> + <string name="loading_image" msgid="1200894415793838191">"Laster inn bilde …"</string> + <string name="loading_account" msgid="928195413034552034">"Laster inn konto …"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Fortsett avspilling"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Fortsett avspilling fra %s?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Fortsett avspilling"</string> + <string name="loading" msgid="7038208555304563571">"Laster inn ..."</string> + <string name="fail_to_load" msgid="2710120770735315683">"Kunne ikke laste inn"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Ingen miniatyrbilder"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Begynn på nytt"</string> + <string name="crop_save_text" msgid="8821167985419282305">"OK"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Trykk på et ansikt for å begynne."</string> + <string name="saving_image" msgid="7270334453636349407">"Lagrer bilde ..."</string> + <string name="crop_label" msgid="521114301871349328">"Beskjær bilde"</string> + <string name="select_image" msgid="7841406150484742140">"Velg bilde"</string> + <string name="select_video" msgid="4859510992798615076">"Velg video"</string> + <string name="select_item" msgid="2257529413100472599">"Velg elementer"</string> + <string name="select_album" msgid="4632641262236697235">"Velg album"</string> + <string name="select_group" msgid="9090385962030340391">"Velg grupper"</string> + <string name="set_image" msgid="2331476809308010401">"Angi bilde som"</string> + <string name="wallpaper" msgid="9222901738515471972">"Setter bakgrunnsbilde, vent litt…"</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Bakgrunnsbilde"</string> + <string name="delete" msgid="2839695998251824487">"Slett"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Bekreft sletting"</string> + <string name="cancel" msgid="3637516880917356226">"Avbryt"</string> + <string name="share" msgid="3619042788254195341">"Del"</string> + <string name="select_all" msgid="8623593677101437957">"Marker alle"</string> + <string name="deselect_all" msgid="7397531298370285581">"Fjern alle markeringer"</string> + <string name="slideshow" msgid="4355906903247112975">"Lysbildevisning"</string> + <string name="details" msgid="8415120088556445230">"Detaljer"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Bytt til kamera"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"Vis på kartet"</string> + <string name="rotate_left" msgid="7412075232752726934">"Roter til venstre"</string> + <string name="rotate_right" msgid="7340681085011826618">"Roter til høyre"</string> + <string name="no_such_item" msgid="3161074758669642065">"Finner ikke artikkelen"</string> + <string name="edit" msgid="1502273844748580847">"Rediger"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Program ikke tilgjengelig"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Behandle forespørsler om bufring"</string> + <string name="caching_label" msgid="3244800874547101776">"Bufrer …"</string> + <string name="crop" msgid="7970750655414797277">"Beskjær"</string> + <string name="set_as" msgid="3636764710790507868">"Angi som"</string> + <string name="video_err" msgid="7917736494827857757">"Kunne ikke spille av videoen"</string> + <string name="group_by_location" msgid="316641628989023253">"Etter posisjon"</string> + <string name="group_by_time" msgid="9046168567717963573">"Etter dato"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Etter etiketter"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Etter personer"</string> + <string name="group_by_album" msgid="1532818636053818958">"Etter album"</string> + <string name="group_by_size" msgid="153766174950394155">"Etter størrelse"</string> + <string name="untagged" msgid="7281481064509590402">"Uten etikett"</string> + <string name="no_location" msgid="2036710947563713111">"Ingen posisjon"</string> + <string name="show_images_only" msgid="7263218480867672653">"Kun bilder"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Kun videoer"</string> + <string name="show_all" msgid="4780647751652596980">"Bilder og videoer"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Fotogalleri"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Ingen bilder"</string> + <string name="crop_saved" msgid="4684933379430649946">"Det beskårede bildet er lagret i nedlasting"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Det beskårede bildet er ikke lagret"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Ingen albumer er tilgjengelige"</string> + <string name="empty_album" msgid="6307897398825514762">"Ingen bilder eller videoer er tilgjengelige"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa Nettalbum"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Gjør tilgjengelig i frakoblet modus"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Ferdig"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d av %2$d elementer:"</string> + <string name="title" msgid="7622928349908052569">"Tittel"</string> + <string name="description" msgid="3016729318096557520">"Beskrivelse"</string> + <string name="time" msgid="1367953006052876956">"Tid"</string> + <string name="location" msgid="3432705876921618314">"Sted"</string> + <string name="path" msgid="4725740395885105824">"Bane"</string> + <string name="width" msgid="9215847239714321097">"Bredde"</string> + <string name="height" msgid="3648885449443787772">"Høyde"</string> + <string name="orientation" msgid="4958327983165245513">"Retning"</string> + <string name="duration" msgid="8160058911218541616">"Varighet"</string> + <string name="mimetype" msgid="3518268469266183548">"MIME-type"</string> + <string name="file_size" msgid="4670384449129762138">"Filstørrelse"</string> + <string name="maker" msgid="7921835498034236197">"Skaper"</string> + <string name="model" msgid="8240207064064337366">"Modell"</string> + <string name="flash" msgid="2816779031261147723">"Blits"</string> + <string name="aperture" msgid="5920657630303915195">"Blender"</string> + <string name="focal_length" msgid="1291383769749877010">"Brennvidde"</string> + <string name="white_balance" msgid="8122534414851280901">"Hvitbalanse"</string> + <string name="exposure_time" msgid="3146642210127439553">"Eksponer.tid"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Manuelt"</string> + <string name="auto" msgid="4296941368722892821">"Auto"</string> + <string name="flash_on" msgid="7891556231891837284">"Blits brukes"</string> + <string name="flash_off" msgid="1445443413822680010">"Uten blits"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Gjør album tilgjengelig i frakoblet modus"</item> + <item quantity="other" msgid="6929905722448632886">"Gjør albumer tilgjengelig i frakoblet modus"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Dette elementet lagres lokalt og er tilgjengelig i frakoblet modus."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Alle albumer"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Lokale albumer"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"MTP-enheter"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Picasa Nettalbum"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> tilgjengelig"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> eller mindre"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> eller mer"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> til <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Importér"</string> + <string name="import_complete" msgid="1098450310074640619">"Importen er fullført"</string> + <string name="import_fail" msgid="5205927625132482529">"Importen mislyktes"</string> + <string name="camera_connected" msgid="6984353643349303075">"Kamera tilkoblet"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Kamera frakoblet"</string> + <string name="click_import" msgid="6407959065464291972">"Trykk her for å importere"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Bilder fra et album"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Stokk om på bildene"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Velg et bilde"</string> + <string name="widget_type" msgid="7308564524449340985">"Modultype"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Lysbildefremvisning"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Forhåndshenter Picasa-bilder:"</string> + <string name="cache_status" msgid="7690438435538533106">"Last ned <xliff:g id="NUMBER_0">%1$s</xliff:g> av <xliff:g id="NUMBER_1">%2$s</xliff:g> bilder"</string> + <string name="cache_done" msgid="9194449192869777483">"Nedlasting er fullført"</string> + <string name="albums" msgid="7320787705180057947">"Album"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"Steder"</string> + <string name="people" msgid="4114003823747292747">"Folk"</string> + <string name="tags" msgid="5539648765482935955">"Etiketter"</string> + <string name="group_by" msgid="4308299657902209357">"Gruppér etter"</string> + <string name="settings" msgid="1534847740615665736">"Innstillinger"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"Kontoinnstillinger"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Innstillinger for databruk"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Automatisk opplasting"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Andre innstillinger"</string> + <string name="about_gallery" msgid="8667445445883757255">"Om galleri"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Synkroniser bare via Wi-Fi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Last automatisk opp alle bildene og videoene du tar, til et privat Picasa nettalbum"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Aktiver automatisk opplasting"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Synkronisering av bilder er PÅ"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Synkronisering av bilder er AV"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Endre innst. for synk. eller slett konto"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Se bilder og videoer fra denne kontoen i galleriet"</string> + <string name="add_account" msgid="4271217504968243974">"Legg til konto"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Velg konto for automatisk opplasting"</string> +</resources> diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml new file mode 100644 index 000000000..49f80c10f --- /dev/null +++ b/res/values-nl/strings.xml @@ -0,0 +1,177 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galerij"</string> + <string name="gadget_title" msgid="259405922673466798">"Fotolijstje"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Videospeler"</string> + <string name="loading_video" msgid="4013492720121891585">"Video laden..."</string> + <string name="loading_image" msgid="1200894415793838191">"Afbeelding laden..."</string> + <string name="loading_account" msgid="928195413034552034">"Account laden???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Video hervatten"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Afspelen hervatten vanaf %s ?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Afspelen hervatten"</string> + <string name="loading" msgid="7038208555304563571">"Laden..."</string> + <string name="fail_to_load" msgid="2710120770735315683">"Laden is mislukt"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Geen miniatuur"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Opnieuw starten"</string> + <string name="crop_save_text" msgid="8821167985419282305">"OK"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Tik op een gezicht om te beginnen."</string> + <string name="saving_image" msgid="7270334453636349407">"Foto opslaan..."</string> + <string name="crop_label" msgid="521114301871349328">"Foto bijsnijden"</string> + <string name="select_image" msgid="7841406150484742140">"Foto selecteren"</string> + <string name="select_video" msgid="4859510992798615076">"Video selecteren"</string> + <string name="select_item" msgid="2257529413100472599">"Item(s) selecteren"</string> + <string name="select_album" msgid="4632641262236697235">"Album(s) selecteren"</string> + <string name="select_group" msgid="9090385962030340391">"Groep(en) selecteren"</string> + <string name="set_image" msgid="2331476809308010401">"Foto instellen als"</string> + <string name="wallpaper" msgid="9222901738515471972">"Achtergrond wordt ingesteld. Een ogenblik geduld..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Achtergrond"</string> + <string name="delete" msgid="2839695998251824487">"Verwijderen"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Verwijderen bevestigen"</string> + <string name="cancel" msgid="3637516880917356226">"Annuleren"</string> + <string name="share" msgid="3619042788254195341">"Delen"</string> + <string name="select_all" msgid="8623593677101437957">"Alles selecteren"</string> + <string name="deselect_all" msgid="7397531298370285581">"Selectie van alle items ongedaan maken"</string> + <string name="slideshow" msgid="4355906903247112975">"Diavoorstelling"</string> + <string name="details" msgid="8415120088556445230">"Details"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Overschakelen naar Camera"</string> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"%1$d geselecteerd"</item> + <item quantity="one" msgid="2478365152745637768">"%1$d geselecteerd"</item> + <item quantity="other" msgid="754722656147810487">"%1$d geselecteerd"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"%1$d geselecteerd"</item> + <item quantity="one" msgid="6184377003099987825">"%1$d geselecteerd"</item> + <item quantity="other" msgid="53105607141906130">"%1$d geselecteerd"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"%1$d geselecteerd"</item> + <item quantity="one" msgid="5030162638216034260">"%1$d geselecteerd"</item> + <item quantity="other" msgid="3512041363942842738">"%1$d geselecteerd"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"Op kaart weergeven"</string> + <string name="rotate_left" msgid="7412075232752726934">"Linksom draaien"</string> + <string name="rotate_right" msgid="7340681085011826618">"Rechtsom draaien"</string> + <string name="no_such_item" msgid="3161074758669642065">"Item is niet gevonden"</string> + <string name="edit" msgid="1502273844748580847">"Bewerken"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Geen app beschikbaar"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Cacheverzoeken verwerken"</string> + <string name="caching_label" msgid="3244800874547101776">"Opslaan..."</string> + <string name="crop" msgid="7970750655414797277">"Bijsnijden"</string> + <string name="set_as" msgid="3636764710790507868">"Instellen als"</string> + <string name="video_err" msgid="7917736494827857757">"Kan video niet afspelen"</string> + <string name="group_by_location" msgid="316641628989023253">"Op locatie"</string> + <string name="group_by_time" msgid="9046168567717963573">"Op tijd"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Op labels"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Op personen"</string> + <string name="group_by_album" msgid="1532818636053818958">"Op album"</string> + <string name="group_by_size" msgid="153766174950394155">"Op grootte"</string> + <string name="untagged" msgid="7281481064509590402">"Geen tags"</string> + <string name="no_location" msgid="2036710947563713111">"Geen locatie"</string> + <string name="show_images_only" msgid="7263218480867672653">"Alleen afbeeldingen"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Alleen video\'s"</string> + <string name="show_all" msgid="4780647751652596980">"Afbeeldingen en video\'s"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Fotogalerij"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Geen foto\'s"</string> + <string name="crop_saved" msgid="4684933379430649946">"Bijgesneden afbeelding is opgeslagen in download"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Bijgesneden afbeelding is niet opgeslagen"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Er zijn geen albums beschikbaar"</string> + <string name="empty_album" msgid="6307897398825514762">"Er zijn geen afbeeldingen/video\'s beschikbaar"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa Webalbums"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Offline beschikbaar maken"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Gereed"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d van %2$d items:"</string> + <string name="title" msgid="7622928349908052569">"Titel"</string> + <string name="description" msgid="3016729318096557520">"Beschrijving"</string> + <string name="time" msgid="1367953006052876956">"Tijd"</string> + <string name="location" msgid="3432705876921618314">"Locatie"</string> + <string name="path" msgid="4725740395885105824">"Pad"</string> + <string name="width" msgid="9215847239714321097">"Breedte"</string> + <string name="height" msgid="3648885449443787772">"Hoogte"</string> + <string name="orientation" msgid="4958327983165245513">"Stand"</string> + <string name="duration" msgid="8160058911218541616">"Duur"</string> + <string name="mimetype" msgid="3518268469266183548">"MIME-type"</string> + <string name="file_size" msgid="4670384449129762138">"Bestandsgrootte"</string> + <string name="maker" msgid="7921835498034236197">"Maker"</string> + <string name="model" msgid="8240207064064337366">"Model"</string> + <string name="flash" msgid="2816779031261147723">"Flits"</string> + <string name="aperture" msgid="5920657630303915195">"Diafragma"</string> + <string name="focal_length" msgid="1291383769749877010">"Brandpuntsafst."</string> + <string name="white_balance" msgid="8122534414851280901">"Witbalans"</string> + <string name="exposure_time" msgid="3146642210127439553">"Belichtingstijd"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Handmatig"</string> + <string name="auto" msgid="4296941368722892821">"Autom."</string> + <string name="flash_on" msgid="7891556231891837284">"Geflitst"</string> + <string name="flash_off" msgid="1445443413822680010">"Geen flits"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Album offline beschikbaar maken"</item> + <item quantity="other" msgid="6929905722448632886">"Albums offline beschikbaar maken"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Dit item is lokaal opgeslagen en offline beschikbaar."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Alle albums"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Lokale albums"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"MTP-apparaten"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Picasa-albums"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> vrij"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> of kleiner"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> of groter"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> tot <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Importeren"</string> + <string name="import_complete" msgid="1098450310074640619">"Importeren voltooid"</string> + <string name="import_fail" msgid="5205927625132482529">"Importeren mislukt"</string> + <string name="camera_connected" msgid="6984353643349303075">"Camera aangesloten"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Camera losgekoppeld"</string> + <string name="click_import" msgid="6407959065464291972">"Raak dit aan om te importeren"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Afbeeldingen uit een album"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Alle afbeeldingen verwisselen"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Een afbeelding kiezen"</string> + <string name="widget_type" msgid="7308564524449340985">"Widgettype"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Diavoorstelling"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Picasa-foto\'s prefetchen:"</string> + <string name="cache_status" msgid="7690438435538533106">"<xliff:g id="NUMBER_0">%1$s</xliff:g> van <xliff:g id="NUMBER_1">%2$s</xliff:g> foto\'s downloaden"</string> + <string name="cache_done" msgid="9194449192869777483">"Downloaden is voltooid"</string> + <string name="albums" msgid="7320787705180057947">"Albums"</string> + <string name="times" msgid="2023033894889499219">"Opnametijden"</string> + <string name="locations" msgid="6649297994083130305">"Locaties"</string> + <string name="people" msgid="4114003823747292747">"Personen"</string> + <string name="tags" msgid="5539648765482935955">"Tags"</string> + <string name="group_by" msgid="4308299657902209357">"Groeperen op"</string> + <string name="settings" msgid="1534847740615665736">"Instellingen"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"Accountinstellingen"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Instellingen voor gegevensgebruik"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Automatisch uploaden"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Overige instellingen"</string> + <string name="about_gallery" msgid="8667445445883757255">"Over Galerij"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Alleen synchroniseren met Wi-Fi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Automatisch alle foto\'s en video\'s die u maakt, uploaden naar een persoonlijk Picasa-webalbum"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Automatisch uploaden inschakelen"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Synchr. Google-foto\'s is AAN"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Synchr. Google-foto\'s is UIT"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Synchron.voork. wijzigen of account verw."</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Foto\'s en video\'s van dit account in Galerij bekijken"</string> + <string name="add_account" msgid="4271217504968243974">"Account toevoegen"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Account voor autom. uploaden"</string> +</resources> diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml new file mode 100644 index 000000000..1a7862f00 --- /dev/null +++ b/res/values-pl/strings.xml @@ -0,0 +1,177 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galeria"</string> + <string name="gadget_title" msgid="259405922673466798">"Ramka ze zdjęciem"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Odtwarzacz wideo"</string> + <string name="loading_video" msgid="4013492720121891585">"Ładowanie filmu..."</string> + <string name="loading_image" msgid="1200894415793838191">"Wczytywanie obrazu…"</string> + <string name="loading_account" msgid="928195413034552034">"Wczytywanie konta..."</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Wznów film"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Wznowić odtwarzanie od %s?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Wznów odtwarzanie"</string> + <string name="loading" msgid="7038208555304563571">"Wczytywanie…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"Ładowanie nie powiodło się"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Brak miniatury"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Rozpocznij"</string> + <string name="crop_save_text" msgid="8821167985419282305">"OK"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Dotknij twarzy, aby rozpocząć"</string> + <string name="saving_image" msgid="7270334453636349407">"Zapisywanie zdjęcia…"</string> + <string name="crop_label" msgid="521114301871349328">"Przytnij zdjęcie"</string> + <string name="select_image" msgid="7841406150484742140">"Wybierz zdjęcie"</string> + <string name="select_video" msgid="4859510992798615076">"Wybierz film"</string> + <string name="select_item" msgid="2257529413100472599">"Wybierz elementy"</string> + <string name="select_album" msgid="4632641262236697235">"Wybierz albumy"</string> + <string name="select_group" msgid="9090385962030340391">"Wybierz grupy"</string> + <string name="set_image" msgid="2331476809308010401">"Ustaw zdjęcie jako"</string> + <string name="wallpaper" msgid="9222901738515471972">"Ustawianie tapety, proszę czekać…"</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Tapeta"</string> + <string name="delete" msgid="2839695998251824487">"Usuń"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Potwierdź usunięcie"</string> + <string name="cancel" msgid="3637516880917356226">"Anuluj"</string> + <string name="share" msgid="3619042788254195341">"Udostępnij"</string> + <string name="select_all" msgid="8623593677101437957">"Zaznacz wszystko"</string> + <string name="deselect_all" msgid="7397531298370285581">"Usuń zaznaczenie wszystkich"</string> + <string name="slideshow" msgid="4355906903247112975">"Pokaz slajdów"</string> + <string name="details" msgid="8415120088556445230">"Szczegóły"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Przełącz na aparat"</string> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"Wybrane: %1$d"</item> + <item quantity="one" msgid="2478365152745637768">"Wybrane: %1$d"</item> + <item quantity="other" msgid="754722656147810487">"Wybrane: %1$d"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"Wybrane: %1$d"</item> + <item quantity="one" msgid="6184377003099987825">"Wybrane: %1$d"</item> + <item quantity="other" msgid="53105607141906130">"Wybrane: %1$d"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"Wybrane: %1$d"</item> + <item quantity="one" msgid="5030162638216034260">"Wybrane: %1$d"</item> + <item quantity="other" msgid="3512041363942842738">"Wybrane: %1$d"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"Pokaż na mapie"</string> + <string name="rotate_left" msgid="7412075232752726934">"Obróć w lewo"</string> + <string name="rotate_right" msgid="7340681085011826618">"Obróć w prawo"</string> + <string name="no_such_item" msgid="3161074758669642065">"Nie znaleziono elementu"</string> + <string name="edit" msgid="1502273844748580847">"Edytuj"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Brak dostępnych aplikacji"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Przetwarzanie żądań dotyczących pamięci podręcznej"</string> + <string name="caching_label" msgid="3244800874547101776">"Trwa buforowanie..."</string> + <string name="crop" msgid="7970750655414797277">"Przytnij"</string> + <string name="set_as" msgid="3636764710790507868">"Ustaw jako"</string> + <string name="video_err" msgid="7917736494827857757">"Nie można odtworzyć filmu wideo"</string> + <string name="group_by_location" msgid="316641628989023253">"Według lokalizacji"</string> + <string name="group_by_time" msgid="9046168567717963573">"Według daty"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Według tagów"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Według osób"</string> + <string name="group_by_album" msgid="1532818636053818958">"Według albumu"</string> + <string name="group_by_size" msgid="153766174950394155">"Wg rozmiaru"</string> + <string name="untagged" msgid="7281481064509590402">"Nieoznaczone tagami"</string> + <string name="no_location" msgid="2036710947563713111">"Brak informacji o lokalizacji"</string> + <string name="show_images_only" msgid="7263218480867672653">"Tylko obrazy"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Tylko filmy"</string> + <string name="show_all" msgid="4780647751652596980">"Obrazy i filmy"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Galeria zdjęć"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Brak zdjęć"</string> + <string name="crop_saved" msgid="4684933379430649946">"Przycięty obraz zapisano wśród pobranych plików"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Przycięty obraz nie został zapisany"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Brak dostępnych albumów"</string> + <string name="empty_album" msgid="6307897398825514762">"Brak dostępnych zdjęć/filmów"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa Web Albums"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Udostępnij w trybie offline"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Gotowe"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d z %2$d elementów:"</string> + <string name="title" msgid="7622928349908052569">"Tytuł"</string> + <string name="description" msgid="3016729318096557520">"Opis"</string> + <string name="time" msgid="1367953006052876956">"Godzina"</string> + <string name="location" msgid="3432705876921618314">"Lokalizacja"</string> + <string name="path" msgid="4725740395885105824">"Ścieżka"</string> + <string name="width" msgid="9215847239714321097">"Szerokość"</string> + <string name="height" msgid="3648885449443787772">"Wysokość"</string> + <string name="orientation" msgid="4958327983165245513">"Orientacja"</string> + <string name="duration" msgid="8160058911218541616">"Czas trwania"</string> + <string name="mimetype" msgid="3518268469266183548">"Typ MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Rozmiar pliku"</string> + <string name="maker" msgid="7921835498034236197">"Producent"</string> + <string name="model" msgid="8240207064064337366">"Model"</string> + <string name="flash" msgid="2816779031261147723">"Flash"</string> + <string name="aperture" msgid="5920657630303915195">"Przesłona"</string> + <string name="focal_length" msgid="1291383769749877010">"Ogniskowa"</string> + <string name="white_balance" msgid="8122534414851280901">"Balans bieli"</string> + <string name="exposure_time" msgid="3146642210127439553">"Czas ekspoz."</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Ręczna"</string> + <string name="auto" msgid="4296941368722892821">"Automat."</string> + <string name="flash_on" msgid="7891556231891837284">"Z lampą"</string> + <string name="flash_off" msgid="1445443413822680010">"Bez lampy"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Udostępnianie albumu w trybie offline"</item> + <item quantity="other" msgid="6929905722448632886">"Udostępnianie albumów w trybie offline"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Ten element jest przechowywany lokalnie i dostępny w trybie offline."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Wszystkie albumy"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Albumy lokalne"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"Urządzenia MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Albumy Picasa"</string> + <string name="free_space_format" msgid="8766337315709161215">"Wolne: <xliff:g id="BYTES">%s</xliff:g>"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> lub mniej"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> lub więcej"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> do <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Importuj"</string> + <string name="import_complete" msgid="1098450310074640619">"Import zakończony"</string> + <string name="import_fail" msgid="5205927625132482529">"Niepowodzenie importu"</string> + <string name="camera_connected" msgid="6984353643349303075">"Aparat podłączony"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Aparat odłączony"</string> + <string name="click_import" msgid="6407959065464291972">"Dotknij tutaj, aby zaimportować"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Zdjęcia z albumu"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Pokazuj losowo wszystkie zdjęcia"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Wybierz obraz"</string> + <string name="widget_type" msgid="7308564524449340985">"Typ widżetu"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Pokaz slajdów"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Pobieranie zdjęć Picasa w tle:"</string> + <string name="cache_status" msgid="7690438435538533106">"Pobieranie zdjęć: <xliff:g id="NUMBER_0">%1$s</xliff:g> z <xliff:g id="NUMBER_1">%2$s</xliff:g>"</string> + <string name="cache_done" msgid="9194449192869777483">"Zakończono pobieranie"</string> + <string name="albums" msgid="7320787705180057947">"Albumy"</string> + <string name="times" msgid="2023033894889499219">"Godziny"</string> + <string name="locations" msgid="6649297994083130305">"Lokaliz."</string> + <string name="people" msgid="4114003823747292747">"Osoby"</string> + <string name="tags" msgid="5539648765482935955">"Tagi"</string> + <string name="group_by" msgid="4308299657902209357">"Grupuj według"</string> + <string name="settings" msgid="1534847740615665736">"Ustawienia"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"Ustawienia konta"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Ustawienia transmisji danych"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Automatyczne przesyłanie"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Inne ustawienia"</string> + <string name="about_gallery" msgid="8667445445883757255">"Galeria – informacje"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Synchronizacja tylko w Wi-Fi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Automatycznie przesyła wszystkie Twoje zdjęcia i filmy do prywatnego albumu w usłudze Picasa Web Albums"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Włącz automatyczne przesyłanie"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Synchronizacja jest WŁĄCZONA"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Synchronizacja jest WYŁĄCZONA"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Zmień ust. synchronizacji lub usuń konto"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Zobacz zdjęcia i filmy z tego konta w Galerii"</string> + <string name="add_account" msgid="4271217504968243974">"Dodaj konto"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Wybierz konto do przesyłania"</string> +</resources> diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml new file mode 100644 index 000000000..b782abd87 --- /dev/null +++ b/res/values-pt-rPT/strings.xml @@ -0,0 +1,172 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galeria"</string> + <string name="gadget_title" msgid="259405922673466798">"Moldura da imagem"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Leitor de vídeo"</string> + <string name="loading_video" msgid="4013492720121891585">"A carregar vídeo..."</string> + <string name="loading_image" msgid="1200894415793838191">"A carregar imagem..."</string> + <string name="loading_account" msgid="928195413034552034">"A carregar conta???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Retomar o vídeo"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Retomar reprodução a partir de %s?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Retomar a reprodução"</string> + <string name="loading" msgid="7038208555304563571">"A carregar..."</string> + <string name="fail_to_load" msgid="2710120770735315683">"Não foi possível carregar"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Sem miniatura"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Recomeçar"</string> + <string name="crop_save_text" msgid="8821167985419282305">"Ok"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Toque num rosto para começar."</string> + <string name="saving_image" msgid="7270334453636349407">"A guardar imagem..."</string> + <string name="crop_label" msgid="521114301871349328">"Recortar imagem"</string> + <string name="select_image" msgid="7841406150484742140">"Seleccionar fotog."</string> + <string name="select_video" msgid="4859510992798615076">"Seleccionar vídeo"</string> + <string name="select_item" msgid="2257529413100472599">"Selecionar item(ns)"</string> + <string name="select_album" msgid="4632641262236697235">"Selecionar álbum(ns)"</string> + <string name="select_group" msgid="9090385962030340391">"Selecionar grupo(s)"</string> + <string name="set_image" msgid="2331476809308010401">"Definir imagem como"</string> + <string name="wallpaper" msgid="9222901738515471972">"A definir a imagem de fundo, aguarde..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Imagem de fundo"</string> + <string name="delete" msgid="2839695998251824487">"Eliminar"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Confirmar eliminação"</string> + <string name="cancel" msgid="3637516880917356226">"Cancelar"</string> + <string name="share" msgid="3619042788254195341">"Partilhar"</string> + <string name="select_all" msgid="8623593677101437957">"Seleccionar tudo"</string> + <string name="deselect_all" msgid="7397531298370285581">"Desmarcar tudo"</string> + <string name="slideshow" msgid="4355906903247112975">"Apresentação de diapositivos"</string> + <string name="details" msgid="8415120088556445230">"Detalhes"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Mudar para Câmara"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"Mostrar no mapa"</string> + <string name="rotate_left" msgid="7412075232752726934">"Rodar para a esquerda"</string> + <string name="rotate_right" msgid="7340681085011826618">"Rodar para a direita"</string> + <string name="no_such_item" msgid="3161074758669642065">"Item não encontrado"</string> + <string name="edit" msgid="1502273844748580847">"Editar"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Não estão disponíveis aplicações"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Processar pedidos de colocação em cache"</string> + <string name="caching_label" msgid="3244800874547101776">"A col. cache..."</string> + <string name="crop" msgid="7970750655414797277">"Recortar"</string> + <string name="set_as" msgid="3636764710790507868">"Definir como"</string> + <string name="video_err" msgid="7917736494827857757">"Não é possível reproduzir vídeo"</string> + <string name="group_by_location" msgid="316641628989023253">"Por localização"</string> + <string name="group_by_time" msgid="9046168567717963573">"Por hora"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Por etiquetas"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Por pessoas"</string> + <string name="group_by_album" msgid="1532818636053818958">"Por álbum"</string> + <string name="group_by_size" msgid="153766174950394155">"Por tamanho"</string> + <string name="untagged" msgid="7281481064509590402">"Sem etiqueta"</string> + <string name="no_location" msgid="2036710947563713111">"Sem localização"</string> + <string name="show_images_only" msgid="7263218480867672653">"Apenas imagens"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Apenas vídeos"</string> + <string name="show_all" msgid="4780647751652596980">"Imagens e vídeos"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Galeria de fotografias"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Não existem fotografias."</string> + <string name="crop_saved" msgid="4684933379430649946">"A imagem recortada foi guardada nas transferências"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"A imagem recortada não foi guardada"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Não estão disponíveis álbuns"</string> + <string name="empty_album" msgid="6307897398825514762">"Não estão disponíveis imagens/vídeos"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Álbuns Web Picasa"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Disponibilizar off-line"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Concluído"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d de %2$d itens:"</string> + <string name="title" msgid="7622928349908052569">"Título"</string> + <string name="description" msgid="3016729318096557520">"Descrição"</string> + <string name="time" msgid="1367953006052876956">"Hora"</string> + <string name="location" msgid="3432705876921618314">"Localização"</string> + <string name="path" msgid="4725740395885105824">"Caminho"</string> + <string name="width" msgid="9215847239714321097">"Largura"</string> + <string name="height" msgid="3648885449443787772">"Altura"</string> + <string name="orientation" msgid="4958327983165245513">"Orientação"</string> + <string name="duration" msgid="8160058911218541616">"Duração"</string> + <string name="mimetype" msgid="3518268469266183548">"Tipo MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Tam. ficheiro"</string> + <string name="maker" msgid="7921835498034236197">"Fabricante"</string> + <string name="model" msgid="8240207064064337366">"Modelo"</string> + <string name="flash" msgid="2816779031261147723">"Flash"</string> + <string name="aperture" msgid="5920657630303915195">"Abertura"</string> + <string name="focal_length" msgid="1291383769749877010">"Dist. focal"</string> + <string name="white_balance" msgid="8122534414851280901">"Equilíbrio dos brancos"</string> + <string name="exposure_time" msgid="3146642210127439553">"Tempo expos."</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Manual"</string> + <string name="auto" msgid="4296941368722892821">"Automático"</string> + <string name="flash_on" msgid="7891556231891837284">"Flash dispar."</string> + <string name="flash_off" msgid="1445443413822680010">"Sem flash"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Disponibilizar um álbum off-line"</item> + <item quantity="other" msgid="6929905722448632886">"Disponibilizar álbuns off-line"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Este artigo está armazenado localmente e está disponível off-line."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Todos os álbuns"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Álbuns locais"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"Dispositivos MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Álbuns Picasa"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> livres"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> ou abaixo"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> ou acima"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> para <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Importar"</string> + <string name="import_complete" msgid="1098450310074640619">"Importação Concluída"</string> + <string name="import_fail" msgid="5205927625132482529">"Falha ao Importar"</string> + <string name="camera_connected" msgid="6984353643349303075">"Câmara ligada"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Câmara desligada"</string> + <string name="click_import" msgid="6407959065464291972">"Toque aqui para importar"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Imagens de um álbum"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Repr. aleat. todas as imagens"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Escolher imagem"</string> + <string name="widget_type" msgid="7308564524449340985">"Tipo de Widget"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Apres. de diap."</string> + <string name="cache_status_title" msgid="8414708919928621485">"Pré-obtenção de fotografias do Picasa:"</string> + <string name="cache_status" msgid="7690438435538533106">"Transferir <xliff:g id="NUMBER_0">%1$s</xliff:g> de <xliff:g id="NUMBER_1">%2$s</xliff:g> fotografias"</string> + <string name="cache_done" msgid="9194449192869777483">"Transferência concluída"</string> + <string name="albums" msgid="7320787705180057947">"Álbuns"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"Localizações"</string> + <string name="people" msgid="4114003823747292747">"Pessoas"</string> + <string name="tags" msgid="5539648765482935955">"Etiquetas"</string> + <string name="group_by" msgid="4308299657902209357">"Agrupar por"</string> + <string name="settings" msgid="1534847740615665736">"Definições"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"Definições da conta"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Definições da utilização de dados"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Carregamento automático"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Outras definições"</string> + <string name="about_gallery" msgid="8667445445883757255">"Acerca da Galeria"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Sincronizar apenas por Wi-Fi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Carregar automaticamente para um álbum Web picasa privado todas as fotografias e vídeos que fizer"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Ativar o Carregamento automático"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Sinc. de fotos Google ativada"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Sinc. de fotos Google desativ."</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Alterar preferências de sincronização ou remover esta conta"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Veja as fotos e vídeos desta conta na Galeria"</string> + <string name="add_account" msgid="4271217504968243974">"Adicionar conta"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Escolh. conta p/ Carreg. autom."</string> +</resources> diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml new file mode 100644 index 000000000..6f2c4ffc2 --- /dev/null +++ b/res/values-pt/strings.xml @@ -0,0 +1,177 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galeria"</string> + <string name="gadget_title" msgid="259405922673466798">"Moldura de uma imagem"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Player de vídeo"</string> + <string name="loading_video" msgid="4013492720121891585">"Carregando vídeo..."</string> + <string name="loading_image" msgid="1200894415793838191">"Carregando imagem…"</string> + <string name="loading_account" msgid="928195413034552034">"Carregando conta..."</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Retomar vídeo"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Retomar reprodução de %s ?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Retomar a reprodução"</string> + <string name="loading" msgid="7038208555304563571">"Carregando..."</string> + <string name="fail_to_load" msgid="2710120770735315683">"Falha ao carregar"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Sem miniatura"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Reiniciar"</string> + <string name="crop_save_text" msgid="8821167985419282305">"Ok"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Toque em um rosto para começar."</string> + <string name="saving_image" msgid="7270334453636349407">"Salvando imagem…"</string> + <string name="crop_label" msgid="521114301871349328">"Cortar imagem"</string> + <string name="select_image" msgid="7841406150484742140">"Selecionar foto"</string> + <string name="select_video" msgid="4859510992798615076">"Selecionar vídeo"</string> + <string name="select_item" msgid="2257529413100472599">"Selecione os itens"</string> + <string name="select_album" msgid="4632641262236697235">"Selecione álbuns"</string> + <string name="select_group" msgid="9090385962030340391">"Selecionar grupos"</string> + <string name="set_image" msgid="2331476809308010401">"Definir imagem como"</string> + <string name="wallpaper" msgid="9222901738515471972">"Configurando o plano de fundo, aguarde…"</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Plano de fundo"</string> + <string name="delete" msgid="2839695998251824487">"Excluir"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Confirmar exclusão"</string> + <string name="cancel" msgid="3637516880917356226">"Cancelar"</string> + <string name="share" msgid="3619042788254195341">"Compartilhar"</string> + <string name="select_all" msgid="8623593677101437957">"Selecionar todos"</string> + <string name="deselect_all" msgid="7397531298370285581">"Desmarcar tudo"</string> + <string name="slideshow" msgid="4355906903247112975">"Apresentação de slides"</string> + <string name="details" msgid="8415120088556445230">"Detalhes"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Alternar para câmera"</string> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"%1$d selecionado(s)"</item> + <item quantity="one" msgid="2478365152745637768">"%1$d selecionado(s)"</item> + <item quantity="other" msgid="754722656147810487">"%1$d selecionado(s)"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"%1$d selecionado(s)"</item> + <item quantity="one" msgid="6184377003099987825">"%1$d selecionado(s)"</item> + <item quantity="other" msgid="53105607141906130">"%1$d selecionado(s)"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"%1$d selecionado(s)"</item> + <item quantity="one" msgid="5030162638216034260">"%1$d selecionado(s)"</item> + <item quantity="other" msgid="3512041363942842738">"%1$d selecionado(s)"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"Mostrar no mapa"</string> + <string name="rotate_left" msgid="7412075232752726934">"Girar para a esquerda"</string> + <string name="rotate_right" msgid="7340681085011826618">"Girar para a direita"</string> + <string name="no_such_item" msgid="3161074758669642065">"Item não encontrado"</string> + <string name="edit" msgid="1502273844748580847">"Editar"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Nenhum aplicativo disponível"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Solicitações de armazenamento de processos"</string> + <string name="caching_label" msgid="3244800874547101776">"Armazenando em cache ..."</string> + <string name="crop" msgid="7970750655414797277">"Cortar"</string> + <string name="set_as" msgid="3636764710790507868">"Definir como"</string> + <string name="video_err" msgid="7917736494827857757">"Não é possível reproduzir o vídeo"</string> + <string name="group_by_location" msgid="316641628989023253">"Por local"</string> + <string name="group_by_time" msgid="9046168567717963573">"Por tempo"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Por tags"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Por pessoas"</string> + <string name="group_by_album" msgid="1532818636053818958">"Por álbum"</string> + <string name="group_by_size" msgid="153766174950394155">"Por tamanho"</string> + <string name="untagged" msgid="7281481064509590402">"Sem tags"</string> + <string name="no_location" msgid="2036710947563713111">"Nenhum local"</string> + <string name="show_images_only" msgid="7263218480867672653">"Apenas imagens"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Somente vídeos"</string> + <string name="show_all" msgid="4780647751652596980">"Imagens e vídeos"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Galeria de fotos"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Nenhuma foto"</string> + <string name="crop_saved" msgid="4684933379430649946">"A imagem cortada foi salva nos downloads"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"A imagem cortada não foi salva"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Não há álbuns disponíveis"</string> + <string name="empty_album" msgid="6307897398825514762">"Não há imagens/vídeos disponíveis"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Álbuns do Picasa"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Tornar disponível off-line"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Concluído"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d de %2$d itens:"</string> + <string name="title" msgid="7622928349908052569">"Título"</string> + <string name="description" msgid="3016729318096557520">"Descrição"</string> + <string name="time" msgid="1367953006052876956">"Horário"</string> + <string name="location" msgid="3432705876921618314">"Local"</string> + <string name="path" msgid="4725740395885105824">"Caminho"</string> + <string name="width" msgid="9215847239714321097">"Largura"</string> + <string name="height" msgid="3648885449443787772">"Altura"</string> + <string name="orientation" msgid="4958327983165245513">"Orientação"</string> + <string name="duration" msgid="8160058911218541616">"Duração"</string> + <string name="mimetype" msgid="3518268469266183548">"Tipo MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Tamanho do arquivo"</string> + <string name="maker" msgid="7921835498034236197">"Criador"</string> + <string name="model" msgid="8240207064064337366">"Modelo"</string> + <string name="flash" msgid="2816779031261147723">"Flash"</string> + <string name="aperture" msgid="5920657630303915195">"Abertura"</string> + <string name="focal_length" msgid="1291383769749877010">"Comprimento focal"</string> + <string name="white_balance" msgid="8122534414851280901">"Bal. de branco"</string> + <string name="exposure_time" msgid="3146642210127439553">"Hora de expos."</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Manual"</string> + <string name="auto" msgid="4296941368722892821">"Auto"</string> + <string name="flash_on" msgid="7891556231891837284">"Flash ativo"</string> + <string name="flash_off" msgid="1445443413822680010">"Sem flash"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Tornar os álbuns disponíveis off-line"</item> + <item quantity="other" msgid="6929905722448632886">"Tornar os álbuns disponíveis off-line"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Este item está armazenado localmente e disponível off-line."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Todos os álbuns"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Álbuns locais"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"Dispositivos MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Álbuns do Picasa"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> gratuito"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> ou menos"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> ou mais"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> para <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Importar"</string> + <string name="import_complete" msgid="1098450310074640619">"Importação concluída"</string> + <string name="import_fail" msgid="5205927625132482529">"Falha na importação"</string> + <string name="camera_connected" msgid="6984353643349303075">"Câmera conectada"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Câmera desconectada"</string> + <string name="click_import" msgid="6407959065464291972">"Toque aqui para importar"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Imagens de um álbum"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Reprod. aleator. as imagens"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Escolha uma imagem"</string> + <string name="widget_type" msgid="7308564524449340985">"Tipo de widget"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Apresent. de slides"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Fazendo pré-busca de fotos do picasa:"</string> + <string name="cache_status" msgid="7690438435538533106">"Download <xliff:g id="NUMBER_0">%1$s</xliff:g> de <xliff:g id="NUMBER_1">%2$s</xliff:g> fotos"</string> + <string name="cache_done" msgid="9194449192869777483">"Download concluído"</string> + <string name="albums" msgid="7320787705180057947">"Álbuns"</string> + <string name="times" msgid="2023033894889499219">"Vezes"</string> + <string name="locations" msgid="6649297994083130305">"Locais"</string> + <string name="people" msgid="4114003823747292747">"Pessoas"</string> + <string name="tags" msgid="5539648765482935955">"Etiquetas"</string> + <string name="group_by" msgid="4308299657902209357">"Agrupar por"</string> + <string name="settings" msgid="1534847740615665736">"Configurações"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"Configurações de conta"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Configurações do uso de dados"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Envio automático"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Outras configurações"</string> + <string name="about_gallery" msgid="8667445445883757255">"Sobre a Galeria"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Sincronizar somente em Wi-Fi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Enviar automaticamente todas as fotos e vídeos colocados em um Álbum do Picasa privado"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Ativar envio automático"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Sincr. do Google Fotos ativada"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Sincr. Google Fotos desativ."</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Alterar pref. de sincr. ou remover conta"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Visualizar fotos e vídeos desta conta na Galeria"</string> + <string name="add_account" msgid="4271217504968243974">"Adicionar conta"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Escolher conta de envio autom."</string> +</resources> diff --git a/res/values-rm/strings.xml b/res/values-rm/strings.xml new file mode 100644 index 000000000..fece3fbda --- /dev/null +++ b/res/values-rm/strings.xml @@ -0,0 +1,272 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Catalog"</string> + <string name="gadget_title" msgid="259405922673466798">"Rom da maletgs"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <!-- outdated translation 3697303290960009886 --> <string name="movie_view_label" msgid="3526526872644898229">"Films"</string> + <string name="loading_video" msgid="4013492720121891585">"Chargiar il video…"</string> + <!-- no translation found for loading_image (1200894415793838191) --> + <skip /> + <!-- no translation found for loading_account (928195413034552034) --> + <skip /> + <string name="resume_playing_title" msgid="8996677350649355013">"Cuntinuar cun il video"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Cuntinuar la reproducziun davent da %s?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Cuntinuar la reproducziun"</string> + <!-- no translation found for loading (7038208555304563571) --> + <skip /> + <!-- no translation found for fail_to_load (2710120770735315683) --> + <skip /> + <!-- no translation found for no_thumbnail (284723185546429750) --> + <skip /> + <string name="resume_playing_restart" msgid="5471008499835769292">"Cumenzar"</string> + <!-- outdated translation 8140440041190264400 --> <string name="crop_save_text" msgid="8821167985419282305">"Memorisar"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Smatgai sin ina fatscha per cumenzar."</string> + <string name="saving_image" msgid="7270334453636349407">"Memorisar il maletg..."</string> + <string name="crop_label" msgid="521114301871349328">"Retagliar il maletg"</string> + <!-- no translation found for select_image (7841406150484742140) --> + <skip /> + <!-- no translation found for select_video (4859510992798615076) --> + <skip /> + <!-- no translation found for select_item (2257529413100472599) --> + <skip /> + <!-- no translation found for select_album (4632641262236697235) --> + <skip /> + <!-- no translation found for select_group (9090385962030340391) --> + <skip /> + <string name="set_image" msgid="2331476809308010401">"Definir il maletg sco"</string> + <string name="wallpaper" msgid="9222901738515471972">"La culissa vegn configurada. Spetgai per plaschair..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Culissa"</string> + <string name="delete" msgid="2839695998251824487">"Stizzar"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Confermar il stizzar"</string> + <string name="cancel" msgid="3637516880917356226">"Interrumper"</string> + <string name="share" msgid="3619042788254195341">"Cundivider"</string> + <string name="select_all" msgid="8623593677101437957">"Selecziunar tut"</string> + <string name="deselect_all" msgid="7397531298370285581">"Deselecziunar tut"</string> + <string name="slideshow" msgid="4355906903247112975">"Preschentaziun da dia"</string> + <string name="details" msgid="8415120088556445230">"Detagls"</string> + <!-- no translation found for switch_to_camera (7280111806675169992) --> + <skip /> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"Mussar sin la charta"</string> + <string name="rotate_left" msgid="7412075232752726934">"Rotar a sanestra"</string> + <string name="rotate_right" msgid="7340681085011826618">"Rotar a dretga"</string> + <!-- no translation found for no_such_item (3161074758669642065) --> + <skip /> + <!-- no translation found for edit (1502273844748580847) --> + <skip /> + <!-- no translation found for activity_not_found (3731390759313019518) --> + <skip /> + <!-- no translation found for process_caching_requests (1076938190997999614) --> + <skip /> + <!-- no translation found for caching_label (3244800874547101776) --> + <skip /> + <string name="crop" msgid="7970750655414797277">"Retagliar"</string> + <string name="set_as" msgid="3636764710790507868">"Definir sco"</string> + <string name="video_err" msgid="7917736494827857757">"Impussibel da reproducir il video"</string> + <!-- no translation found for group_by_location (316641628989023253) --> + <skip /> + <!-- no translation found for group_by_time (9046168567717963573) --> + <skip /> + <!-- no translation found for group_by_tags (3568731317210676160) --> + <skip /> + <!-- no translation found for group_by_faces (1566351636227274906) --> + <skip /> + <!-- no translation found for group_by_album (1532818636053818958) --> + <skip /> + <!-- no translation found for group_by_size (153766174950394155) --> + <skip /> + <!-- no translation found for untagged (7281481064509590402) --> + <skip /> + <!-- no translation found for no_location (2036710947563713111) --> + <skip /> + <!-- no translation found for show_images_only (7263218480867672653) --> + <skip /> + <!-- no translation found for show_videos_only (3850394623678871697) --> + <skip /> + <!-- no translation found for show_all (4780647751652596980) --> + <skip /> + <!-- no translation found for appwidget_title (6410561146863700411) --> + <skip /> + <!-- no translation found for appwidget_empty_text (4123016777080388680) --> + <skip /> + <!-- no translation found for crop_saved (4684933379430649946) --> + <skip /> + <!-- no translation found for crop_not_saved (1438309290700431923) --> + <skip /> + <!-- no translation found for no_albums_alert (3459550423604532470) --> + <skip /> + <!-- no translation found for empty_album (6307897398825514762) --> + <skip /> + <!-- no translation found for picasa_web_albums (5167008066827481663) --> + <skip /> + <!-- no translation found for picasa_posts (1055151689217481993) --> + <skip /> + <!-- no translation found for make_available_offline (5157950985488297112) --> + <skip /> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <!-- no translation found for done (217672440064436595) --> + <skip /> + <!-- no translation found for sequence_in_set (7235465319919457488) --> + <skip /> + <string name="title" msgid="7622928349908052569">"Titel"</string> + <!-- no translation found for description (3016729318096557520) --> + <skip /> + <!-- no translation found for time (1367953006052876956) --> + <skip /> + <string name="location" msgid="3432705876921618314">"Lieu"</string> + <!-- no translation found for path (4725740395885105824) --> + <skip /> + <!-- no translation found for width (9215847239714321097) --> + <skip /> + <!-- no translation found for height (3648885449443787772) --> + <skip /> + <!-- no translation found for orientation (4958327983165245513) --> + <skip /> + <!-- no translation found for duration (8160058911218541616) --> + <skip /> + <!-- no translation found for mimetype (3518268469266183548) --> + <skip /> + <!-- no translation found for file_size (4670384449129762138) --> + <skip /> + <!-- no translation found for maker (7921835498034236197) --> + <skip /> + <!-- no translation found for model (8240207064064337366) --> + <skip /> + <!-- no translation found for flash (2816779031261147723) --> + <skip /> + <!-- no translation found for aperture (5920657630303915195) --> + <skip /> + <!-- no translation found for focal_length (1291383769749877010) --> + <skip /> + <!-- no translation found for white_balance (8122534414851280901) --> + <skip /> + <!-- no translation found for exposure_time (3146642210127439553) --> + <skip /> + <!-- no translation found for iso (5028296664327335940) --> + <skip /> + <!-- no translation found for unit_mm (1125768433254329136) --> + <skip /> + <!-- no translation found for manual (6608905477477607865) --> + <skip /> + <!-- no translation found for auto (4296941368722892821) --> + <skip /> + <!-- no translation found for flash_on (7891556231891837284) --> + <skip /> + <!-- no translation found for flash_off (1445443413822680010) --> + <skip /> + <!-- no translation found for make_albums_available_offline:one (2955975726887896888) --> + <!-- no translation found for make_albums_available_offline:other (6929905722448632886) --> + <!-- no translation found for try_to_set_local_album_available_offline (2184754031896160755) --> + <skip /> + <!-- no translation found for set_label_all_albums (3507256844918130594) --> + <skip /> + <!-- no translation found for set_label_local_albums (5227548825039781) --> + <skip /> + <!-- no translation found for set_label_mtp_devices (5779788799122828528) --> + <skip /> + <!-- no translation found for set_label_picasa_albums (2736308697306982589) --> + <skip /> + <!-- no translation found for free_space_format (8766337315709161215) --> + <skip /> + <!-- no translation found for size_below (2074956730721942260) --> + <skip /> + <!-- no translation found for size_above (5324398253474104087) --> + <skip /> + <!-- no translation found for size_between (8779660840898917208) --> + <skip /> + <!-- no translation found for Import (3985447518557474672) --> + <skip /> + <!-- no translation found for import_complete (1098450310074640619) --> + <skip /> + <!-- no translation found for import_fail (5205927625132482529) --> + <skip /> + <!-- no translation found for camera_connected (6984353643349303075) --> + <skip /> + <!-- no translation found for camera_disconnected (3683036560562699311) --> + <skip /> + <!-- no translation found for click_import (6407959065464291972) --> + <skip /> + <!-- no translation found for widget_type_album (3245149644830731121) --> + <skip /> + <!-- no translation found for widget_type_shuffle (8594622705019763768) --> + <skip /> + <!-- no translation found for widget_type_photo (8384174698965738770) --> + <skip /> + <!-- no translation found for widget_type (7308564524449340985) --> + <skip /> + <!-- no translation found for slideshow_dream_name (6915963319933437083) --> + <skip /> + <!-- no translation found for cache_status_title (8414708919928621485) --> + <skip /> + <!-- no translation found for cache_status (7690438435538533106) --> + <skip /> + <!-- no translation found for cache_done (9194449192869777483) --> + <skip /> + <!-- no translation found for albums (7320787705180057947) --> + <skip /> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <!-- no translation found for locations (6649297994083130305) --> + <skip /> + <!-- no translation found for people (4114003823747292747) --> + <skip /> + <!-- no translation found for tags (5539648765482935955) --> + <skip /> + <!-- no translation found for group_by (4308299657902209357) --> + <skip /> + <string name="settings" msgid="1534847740615665736">"Parameters"</string> + <!-- no translation found for prefs_accounts (7942761992713671670) --> + <skip /> + <!-- no translation found for prefs_data_usage (410592732727343215) --> + <skip /> + <!-- no translation found for prefs_auto_upload (2467627128066665126) --> + <skip /> + <!-- no translation found for prefs_other_settings (6034181851440646681) --> + <skip /> + <!-- no translation found for about_gallery (8667445445883757255) --> + <skip /> + <!-- no translation found for sync_on_wifi_only (5795753226259399958) --> + <skip /> + <!-- no translation found for helptext_auto_upload (133741242503097377) --> + <skip /> + <!-- no translation found for enable_auto_upload (1586329406342131) --> + <skip /> + <!-- no translation found for photo_sync_is_on (1653898269297050634) --> + <skip /> + <!-- no translation found for photo_sync_is_off (6464193461664544289) --> + <skip /> + <!-- no translation found for helptext_photo_sync (8617245939103545623) --> + <skip /> + <!-- no translation found for view_photo_for_account (5608040380422337939) --> + <skip /> + <!-- no translation found for add_account (4271217504968243974) --> + <skip /> + <!-- no translation found for auto_upload_chooser_title (1494524693870792948) --> + <skip /> +</resources> diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml new file mode 100644 index 000000000..7a2412a79 --- /dev/null +++ b/res/values-ro/strings.xml @@ -0,0 +1,173 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galerie"</string> + <string name="gadget_title" msgid="259405922673466798">"Ramă foto"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Player video"</string> + <string name="loading_video" msgid="4013492720121891585">"Se încarcă videoclipul..."</string> + <string name="loading_image" msgid="1200894415793838191">"Se încarcă imaginea..."</string> + <string name="loading_account" msgid="928195413034552034">"Contul se încarcă..."</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Reluaţi videoclipul"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Reluaţi redarea de la %s?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Reluaţi redarea"</string> + <string name="loading" msgid="7038208555304563571">"Se încarcă..."</string> + <string name="fail_to_load" msgid="2710120770735315683">"Eroare la încărcare"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Nu există o miniatură"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Începeţi din nou"</string> + <string name="crop_save_text" msgid="8821167985419282305">"Ok"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Apăsaţi pe un chip pentru a începe."</string> + <string name="saving_image" msgid="7270334453636349407">"Se salvează fotografia..."</string> + <string name="crop_label" msgid="521114301871349328">"Decupaţi fotografia"</string> + <string name="select_image" msgid="7841406150484742140">"Selectaţi fotografie"</string> + <string name="select_video" msgid="4859510992798615076">"Selectaţi videoclip"</string> + <string name="select_item" msgid="2257529413100472599">"Selectaţi elemente"</string> + <string name="select_album" msgid="4632641262236697235">"Selectaţi albume"</string> + <string name="select_group" msgid="9090385962030340391">"Selectaţi grupuri"</string> + <string name="set_image" msgid="2331476809308010401">"Setaţi fotografia ca"</string> + <string name="wallpaper" msgid="9222901738515471972">"Se setează imaginea de fundal. Aşteptaţi..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Imagine de fundal"</string> + <string name="delete" msgid="2839695998251824487">"Ştergeţi"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Confirmaţi ştergerea"</string> + <string name="cancel" msgid="3637516880917356226">"Anulaţi"</string> + <string name="share" msgid="3619042788254195341">"Distribuiţi"</string> + <string name="select_all" msgid="8623593677101437957">"Selectaţi-le pe toate"</string> + <string name="deselect_all" msgid="7397531298370285581">"Deselectaţi-le pe toate"</string> + <string name="slideshow" msgid="4355906903247112975">"Prezentare"</string> + <string name="details" msgid="8415120088556445230">"Detalii"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Comutaţi la Camera foto"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"Afişaţi pe hartă"</string> + <string name="rotate_left" msgid="7412075232752726934">"Rotiţi spre stânga"</string> + <string name="rotate_right" msgid="7340681085011826618">"Rotiţi spre dreapta"</string> + <string name="no_such_item" msgid="3161074758669642065">"Elementul nu a fost găsit"</string> + <string name="edit" msgid="1502273844748580847">"Editaţi"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Nu există aplicaţii disponibile"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Se procesează solic. de stocare în memoria cache"</string> + <string name="caching_label" msgid="3244800874547101776">"Mem. cache..."</string> + <string name="crop" msgid="7970750655414797277">"Decupaţi"</string> + <string name="set_as" msgid="3636764710790507868">"Setaţi ca"</string> + <string name="video_err" msgid="7917736494827857757">"Videoclipul nu poate fi redat"</string> + <string name="group_by_location" msgid="316641628989023253">"După locaţie"</string> + <string name="group_by_time" msgid="9046168567717963573">"După dată"</string> + <string name="group_by_tags" msgid="3568731317210676160">"După etichete"</string> + <string name="group_by_faces" msgid="1566351636227274906">"În funcţie de persoane"</string> + <string name="group_by_album" msgid="1532818636053818958">"După album"</string> + <string name="group_by_size" msgid="153766174950394155">"În funcţie de dimensiune"</string> + <string name="untagged" msgid="7281481064509590402">"Neetichetate"</string> + <string name="no_location" msgid="2036710947563713111">"Fără locaţie"</string> + <string name="show_images_only" msgid="7263218480867672653">"Numai imagini"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Numai videoclipuri"</string> + <string name="show_all" msgid="4780647751652596980">"Imagini şi videoclipuri"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Galerie foto"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Nu există fotografii"</string> + <string name="crop_saved" msgid="4684933379430649946">"Imaginea decupată s-a salvat în dosarul descărcare"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Imaginea decupată nu s-a salvat"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Nu există albume disponibile"</string> + <string name="empty_album" msgid="6307897398825514762">"Nu există imagini/videoclipuri disponibile"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa Web Albums"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Faceţi-le disponibile offline"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Terminat"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d din %2$d (de) elemente:"</string> + <string name="title" msgid="7622928349908052569">"Titlu"</string> + <string name="description" msgid="3016729318096557520">"Descriere"</string> + <string name="time" msgid="1367953006052876956">"Oră"</string> + <string name="location" msgid="3432705876921618314">"Locaţie"</string> + <string name="path" msgid="4725740395885105824">"Cale"</string> + <string name="width" msgid="9215847239714321097">"Lăţime"</string> + <string name="height" msgid="3648885449443787772">"Înălţime"</string> + <string name="orientation" msgid="4958327983165245513">"Orientare"</string> + <string name="duration" msgid="8160058911218541616">"Durată"</string> + <string name="mimetype" msgid="3518268469266183548">"Tip MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Dim. fişier"</string> + <string name="maker" msgid="7921835498034236197">"Producător"</string> + <string name="model" msgid="8240207064064337366">"Model"</string> + <string name="flash" msgid="2816779031261147723">"Bliţ"</string> + <string name="aperture" msgid="5920657630303915195">"Diafragmă"</string> + <string name="focal_length" msgid="1291383769749877010">"Dist. focală"</string> + <string name="white_balance" msgid="8122534414851280901">"Balans de alb"</string> + <string name="exposure_time" msgid="3146642210127439553">"Timp expunere"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Manual"</string> + <string name="auto" msgid="4296941368722892821">"Auto"</string> + <string name="flash_on" msgid="7891556231891837284">"Bliţ activat"</string> + <string name="flash_off" msgid="1445443413822680010">"Fără bliţ"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Se face disponibil un album offline"</item> + <item quantity="other" msgid="6929905722448632886">"Se fac disponibile albume offline"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Acest element este stocat local şi disponibil offline."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Toate albumele"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Albume locale"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"Dispozitive MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Albume Picasa"</string> + <string name="free_space_format" msgid="8766337315709161215">"Spaţiu liber: <xliff:g id="BYTES">%s</xliff:g>"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> sau mai puţin"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> sau mai mult"</string> + <string name="size_between" msgid="8779660840898917208">"Între <xliff:g id="MIN_SIZE">%1$s</xliff:g> şi <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Importaţi"</string> + <string name="import_complete" msgid="1098450310074640619">"Import finalizat"</string> + <string name="import_fail" msgid="5205927625132482529">"Importul nu a reuşit"</string> + <string name="camera_connected" msgid="6984353643349303075">"Camera foto conectată"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Camera foto deconectată"</string> + <string name="click_import" msgid="6407959065464291972">"Atingeţi aici pentru import"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Imagini dintr-un album"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Redaţi aleatoriu toate imag."</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Alegeţi o imagine"</string> + <string name="widget_type" msgid="7308564524449340985">"Tip de obiect widget"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Slideshow"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Preîncărcare fotografii Picasa:"</string> + <string name="cache_status" msgid="7690438435538533106">"Descarcă <xliff:g id="NUMBER_0">%1$s</xliff:g> din <xliff:g id="NUMBER_1">%2$s</xliff:g> (de) fotografii"</string> + <string name="cache_done" msgid="9194449192869777483">"Descărcare completă"</string> + <string name="albums" msgid="7320787705180057947">"Albume"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"Locaţii"</string> + <string name="people" msgid="4114003823747292747">"Persoane"</string> + <string name="tags" msgid="5539648765482935955">"Etichete"</string> + <string name="group_by" msgid="4308299657902209357">"Grupaţi după"</string> + <!-- no translation found for settings (1534847740615665736) --> + <skip /> + <string name="prefs_accounts" msgid="7942761992713671670">"Setările contului"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Setări pentru utilizarea datelor"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Încărcare automată"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Alte setări"</string> + <string name="about_gallery" msgid="8667445445883757255">"Despre Galerie"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Sincronizare numai pe Wi-Fi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Încărcaţi automat toate fotografiile şi videoclipurile pe care le realizaţi într-un album web Picasa privat"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Activaţi încărcarea automată"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Sinc. fotogr. Google ACTIVATĂ"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Sinc. foto. Google DEZACTIVATĂ"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Modif. pref. de sincr. sau elim. contul"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Afişaţi fotografii şi videoclipuri din acest cont în Galerie"</string> + <string name="add_account" msgid="4271217504968243974">"Adăugaţi un cont"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Aleg. cont pt. încărc. autom."</string> +</resources> diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml new file mode 100644 index 000000000..6a52e9887 --- /dev/null +++ b/res/values-ru/strings.xml @@ -0,0 +1,172 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Галерея"</string> + <string name="gadget_title" msgid="259405922673466798">"Рамка фотографии"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Видеопроигрыватель"</string> + <string name="loading_video" msgid="4013492720121891585">"Загрузка видео…"</string> + <string name="loading_image" msgid="1200894415793838191">"Загрузка изображения..."</string> + <string name="loading_account" msgid="928195413034552034">"Загрузка аккаунта..."</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Продолжение просмотра видео"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Продолжить воспроизведение с %s?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Продолжить воспроизведение"</string> + <string name="loading" msgid="7038208555304563571">"Идет загрузка…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"Не удалось загрузить"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Нет уменьшенного изображения"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Начать с начала"</string> + <string name="crop_save_text" msgid="8821167985419282305">"ОК"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Нажмите лицо, чтобы начать."</string> + <string name="saving_image" msgid="7270334453636349407">"Сохранение картинки..."</string> + <string name="crop_label" msgid="521114301871349328">"Обрезать фотографию"</string> + <string name="select_image" msgid="7841406150484742140">"Выберите фотографию"</string> + <string name="select_video" msgid="4859510992798615076">"Выберите видео"</string> + <string name="select_item" msgid="2257529413100472599">"Выберите объекты"</string> + <string name="select_album" msgid="4632641262236697235">"Выберите альбомы"</string> + <string name="select_group" msgid="9090385962030340391">"Выберите группы"</string> + <string name="set_image" msgid="2331476809308010401">"Установить картинку как"</string> + <string name="wallpaper" msgid="9222901738515471972">"Установка обоев, подождите..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Обои"</string> + <string name="delete" msgid="2839695998251824487">"Удалить"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Подтвердить удаление"</string> + <string name="cancel" msgid="3637516880917356226">"Отмена"</string> + <string name="share" msgid="3619042788254195341">"Отправить"</string> + <string name="select_all" msgid="8623593677101437957">"Все"</string> + <string name="deselect_all" msgid="7397531298370285581">"Ни одного"</string> + <string name="slideshow" msgid="4355906903247112975">"Слайд-шоу"</string> + <string name="details" msgid="8415120088556445230">"Сведения"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Переключиться на камеру"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"Показать на карте"</string> + <string name="rotate_left" msgid="7412075232752726934">"Повернуть влево"</string> + <string name="rotate_right" msgid="7340681085011826618">"Повернуть вправо"</string> + <string name="no_such_item" msgid="3161074758669642065">"Файл не найден"</string> + <string name="edit" msgid="1502273844748580847">"Изменить"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Нет доступных приложений"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Запросы на кэширование процессов"</string> + <string name="caching_label" msgid="3244800874547101776">"Кэширование..."</string> + <string name="crop" msgid="7970750655414797277">"Обрезать"</string> + <string name="set_as" msgid="3636764710790507868">"Установить как"</string> + <string name="video_err" msgid="7917736494827857757">"Не удается воспроизвести видео"</string> + <string name="group_by_location" msgid="316641628989023253">"По местоположению"</string> + <string name="group_by_time" msgid="9046168567717963573">"По времени"</string> + <string name="group_by_tags" msgid="3568731317210676160">"По тегам"</string> + <string name="group_by_faces" msgid="1566351636227274906">"По людям"</string> + <string name="group_by_album" msgid="1532818636053818958">"По альбомам"</string> + <string name="group_by_size" msgid="153766174950394155">"По размеру"</string> + <string name="untagged" msgid="7281481064509590402">"Без тегов"</string> + <string name="no_location" msgid="2036710947563713111">"Местоположение неизвестно"</string> + <string name="show_images_only" msgid="7263218480867672653">"Только изображения"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Только видео"</string> + <string name="show_all" msgid="4780647751652596980">"Изображения и видео"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Фотогалерея"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Нет фотографий"</string> + <string name="crop_saved" msgid="4684933379430649946">"Кадрированное изображение сохранено в \"Загрузки\""</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Кадрированное изображение не сохранено"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Нет доступных альбомов"</string> + <string name="empty_album" msgid="6307897398825514762">"Нет доступных изображений и видео"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Веб-альбомы Picasa"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Живая лента Google"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Сделать доступным офлайн"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Готово"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d из %2$d:"</string> + <string name="title" msgid="7622928349908052569">"Название"</string> + <string name="description" msgid="3016729318096557520">"Описание"</string> + <string name="time" msgid="1367953006052876956">"Время"</string> + <string name="location" msgid="3432705876921618314">"Местоположение"</string> + <string name="path" msgid="4725740395885105824">"Путь"</string> + <string name="width" msgid="9215847239714321097">"Ширина"</string> + <string name="height" msgid="3648885449443787772">"Высота"</string> + <string name="orientation" msgid="4958327983165245513">"Ориентация"</string> + <string name="duration" msgid="8160058911218541616">"Длительность"</string> + <string name="mimetype" msgid="3518268469266183548">"Тип MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Размер файла"</string> + <string name="maker" msgid="7921835498034236197">"Автор:"</string> + <string name="model" msgid="8240207064064337366">"Модель"</string> + <string name="flash" msgid="2816779031261147723">"Вспышка"</string> + <string name="aperture" msgid="5920657630303915195">"Диафрагма"</string> + <string name="focal_length" msgid="1291383769749877010">"Фокус. расст."</string> + <string name="white_balance" msgid="8122534414851280901">"Баланс белого"</string> + <string name="exposure_time" msgid="3146642210127439553">"Выдержка"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"мм"</string> + <string name="manual" msgid="6608905477477607865">"Вручную"</string> + <string name="auto" msgid="4296941368722892821">"Авто"</string> + <string name="flash_on" msgid="7891556231891837284">"Со вспышкой"</string> + <string name="flash_off" msgid="1445443413822680010">"Без вспышки"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Загрузка автономного альбома"</item> + <item quantity="other" msgid="6929905722448632886">"Загрузка автономной коллекции альбомов"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Это содержание хранится на устройстве и доступно в автономном режиме."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Все альбомы"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Офлайн-альбомы"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"MTP-устройства"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Альбомы Picasa"</string> + <string name="free_space_format" msgid="8766337315709161215">"Свободно: <xliff:g id="BYTES">%s</xliff:g>"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> или менее"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> или более"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> – <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Импорт"</string> + <string name="import_complete" msgid="1098450310074640619">"Импорт завершен"</string> + <string name="import_fail" msgid="5205927625132482529">"Ошибка импорта"</string> + <string name="camera_connected" msgid="6984353643349303075">"Камера подключена"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Камера отключена"</string> + <string name="click_import" msgid="6407959065464291972">"Нажмите здесь, чтобы начать импорт"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Изображения из альбома"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Перемешать все изображения"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Выбрать картинку"</string> + <string name="widget_type" msgid="7308564524449340985">"Тип виджета"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Слайд-шоу"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Предвыборка фотографий Picasa:"</string> + <string name="cache_status" msgid="7690438435538533106">"Загружено фотографий: <xliff:g id="NUMBER_0">%1$s</xliff:g> из <xliff:g id="NUMBER_1">%2$s</xliff:g>"</string> + <string name="cache_done" msgid="9194449192869777483">"Загрузка завершена"</string> + <string name="albums" msgid="7320787705180057947">"Альбомы"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"Места"</string> + <string name="people" msgid="4114003823747292747">"Люди"</string> + <string name="tags" msgid="5539648765482935955">"Теги"</string> + <string name="group_by" msgid="4308299657902209357">"Группировать по"</string> + <string name="settings" msgid="1534847740615665736">"Настройки"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"Настройки аккаунта"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Настройки использования данных"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Автозагрузка"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Другие настройки"</string> + <string name="about_gallery" msgid="8667445445883757255">"О галерее"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Синхронизация только через Wi-Fi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Автозагрузка фотографий и видеороликов в личный веб-альбом Picasa"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Включить автозагрузку"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Синхронизация фото включена"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Синхронизация фото отключена"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Настройка синхр. или удаление аккаунта"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Просмотр фотографий и видеороликов этого аккаунта в галерее"</string> + <string name="add_account" msgid="4271217504968243974">"Добавить аккаунт"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Аккаунт для автозагрузки"</string> +</resources> diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml new file mode 100644 index 000000000..591234645 --- /dev/null +++ b/res/values-sk/strings.xml @@ -0,0 +1,178 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galéria"</string> + <string name="gadget_title" msgid="259405922673466798">"Rámec fotografie"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Prehrávač videa"</string> + <string name="loading_video" msgid="4013492720121891585">"Prebieha načítavanie videa…"</string> + <string name="loading_image" msgid="1200894415793838191">"Prebieha načítavanie obrázka..."</string> + <string name="loading_account" msgid="928195413034552034">"Načítavanie účtu???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Obnoviť prehrávanie videa"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Pokračovať v prehrávaní od %s?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Obnoviť prehrávanie"</string> + <string name="loading" msgid="7038208555304563571">"Prebieha načítavanie…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"Načítanie zlyhalo"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Žiadne miniatúry"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Začať odznova"</string> + <string name="crop_save_text" msgid="8821167985419282305">"Ok"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Začnite klepnutím na tvár."</string> + <string name="saving_image" msgid="7270334453636349407">"Prebieha ukladanie fotografie..."</string> + <string name="crop_label" msgid="521114301871349328">"Orezať fotografiu"</string> + <string name="select_image" msgid="7841406150484742140">"Vyberte fotografiu"</string> + <string name="select_video" msgid="4859510992798615076">"Vyberte video"</string> + <string name="select_item" msgid="2257529413100472599">"Vyberte položky"</string> + <string name="select_album" msgid="4632641262236697235">"Vyberte albumy"</string> + <string name="select_group" msgid="9090385962030340391">"Vyberte skupiny"</string> + <string name="set_image" msgid="2331476809308010401">"Fotografia bude použitá ako"</string> + <string name="wallpaper" msgid="9222901738515471972">"Prebieha nastavenie tapety, čakajte prosím..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Tapeta"</string> + <string name="delete" msgid="2839695998251824487">"Odstrániť"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Potvrdiť odstránenie"</string> + <string name="cancel" msgid="3637516880917356226">"Zrušiť"</string> + <string name="share" msgid="3619042788254195341">"Zdieľať"</string> + <string name="select_all" msgid="8623593677101437957">"Vybrať všetko"</string> + <string name="deselect_all" msgid="7397531298370285581">"Zrušiť výber všetkých položiek"</string> + <string name="slideshow" msgid="4355906903247112975">"Prezentácia"</string> + <string name="details" msgid="8415120088556445230">"Podrobnosti"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Prepnúť do režimu fotoaparát"</string> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"Počet vybratých položiek: %1$d"</item> + <item quantity="one" msgid="2478365152745637768">"Počet vybratých položiek: %1$d"</item> + <item quantity="other" msgid="754722656147810487">"Počet vybratých položiek: %1$d"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"Počet vybratých albumov: %1$d"</item> + <item quantity="one" msgid="6184377003099987825">"Počet vybratých albumov: %1$d"</item> + <item quantity="other" msgid="53105607141906130">"Počet vybratých albumov: %1$d"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"Počet vybratých skupín: %1$d"</item> + <item quantity="one" msgid="5030162638216034260">"Počet vybratých skupín: %1$d"</item> + <item quantity="other" msgid="3512041363942842738">"Počet vybratých skupín: %1$d"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"Zobraziť na mape"</string> + <string name="rotate_left" msgid="7412075232752726934">"Otočiť doľava"</string> + <string name="rotate_right" msgid="7340681085011826618">"Otočiť doprava"</string> + <string name="no_such_item" msgid="3161074758669642065">"Položka sa nenašla"</string> + <string name="edit" msgid="1502273844748580847">"Upraviť"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Nie sú k dispozícii žiadne aplikácie"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Spracovanie žiadostí o uloženie do vyrov. pamäte"</string> + <string name="caching_label" msgid="3244800874547101776">"Ukladanie do vyrovnávacej pamäte..."</string> + <string name="crop" msgid="7970750655414797277">"Orezať"</string> + <string name="set_as" msgid="3636764710790507868">"Použiť ako"</string> + <string name="video_err" msgid="7917736494827857757">"Video nie je možné prehrať"</string> + <string name="group_by_location" msgid="316641628989023253">"Podľa miesta"</string> + <string name="group_by_time" msgid="9046168567717963573">"Podľa času"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Podľa značiek"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Podľa ľudí"</string> + <string name="group_by_album" msgid="1532818636053818958">"Podľa albumu"</string> + <string name="group_by_size" msgid="153766174950394155">"Podľa veľkosti"</string> + <string name="untagged" msgid="7281481064509590402">"Neoznačené"</string> + <string name="no_location" msgid="2036710947563713111">"Žiadna poloha"</string> + <string name="show_images_only" msgid="7263218480867672653">"Iba obrázky"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Iba videá"</string> + <string name="show_all" msgid="4780647751652596980">"Obrázky a videá"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Fotogaléria"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Žiadne fotografie"</string> + <string name="crop_saved" msgid="4684933379430649946">"Orezaný obrázok bol ulož. do prieč. prevz. súborov"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Orezaný obrázok nie je uložený"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Nie sú k dispozícii žiadne albumy"</string> + <string name="empty_album" msgid="6307897398825514762">"Nie sú k dispozícii žiadne obrázky ani videá"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Webové albumy programu Picasa"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Sprístupniť offline"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Hotovo"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d z %2$d položiek:"</string> + <string name="title" msgid="7622928349908052569">"Titul"</string> + <string name="description" msgid="3016729318096557520">"Popis"</string> + <string name="time" msgid="1367953006052876956">"Čas"</string> + <string name="location" msgid="3432705876921618314">"Poloha"</string> + <string name="path" msgid="4725740395885105824">"Cesta"</string> + <string name="width" msgid="9215847239714321097">"Šírka"</string> + <string name="height" msgid="3648885449443787772">"Výška"</string> + <string name="orientation" msgid="4958327983165245513">"Orientácia"</string> + <string name="duration" msgid="8160058911218541616">"Trvanie"</string> + <string name="mimetype" msgid="3518268469266183548">"Typ MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Veľkosť súboru"</string> + <string name="maker" msgid="7921835498034236197">"Autor"</string> + <string name="model" msgid="8240207064064337366">"Model"</string> + <string name="flash" msgid="2816779031261147723">"Blesk"</string> + <string name="aperture" msgid="5920657630303915195">"Clona"</string> + <string name="focal_length" msgid="1291383769749877010">"Ohnisk. vzd."</string> + <string name="white_balance" msgid="8122534414851280901">"Vyváž. bielej"</string> + <string name="exposure_time" msgid="3146642210127439553">"Doba expozície"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Ručne"</string> + <string name="auto" msgid="4296941368722892821">"Auto"</string> + <string name="flash_on" msgid="7891556231891837284">"S bleskom"</string> + <string name="flash_off" msgid="1445443413822680010">"Bez blesku"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Sprístupňovanie albumu v režime offline"</item> + <item quantity="other" msgid="6929905722448632886">"Sprístupňovanie albumov v režime offline"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Táto položka je uložená miestne a je k dispozícii v režime offline."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Všetky albumy"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Miestne albumy"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"Zariadenie s MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Albumy Picasa"</string> + <string name="free_space_format" msgid="8766337315709161215">"Voľná pamäť: <xliff:g id="BYTES">%s</xliff:g>"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> alebo menej"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> alebo viac"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> až <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Import"</string> + <string name="import_complete" msgid="1098450310074640619">"Import je dokončený"</string> + <string name="import_fail" msgid="5205927625132482529">"Import zlyhal"</string> + <string name="camera_connected" msgid="6984353643349303075">"Fotoaparát bol pripojený"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Fotoaparát bol odpojený"</string> + <string name="click_import" msgid="6407959065464291972">"Ak chcete spustiť import, dotknite sa tu"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Obrázky z albumu"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Náhodné poradie obrázkov"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Vyberte obrázok"</string> + <string name="widget_type" msgid="7308564524449340985">"Typ miniaplikácie"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Prezentácia"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Predbežne načítať fotografie Picasa:"</string> + <string name="cache_status" msgid="7690438435538533106">"Prevziať <xliff:g id="NUMBER_0">%1$s</xliff:g> z <xliff:g id="NUMBER_1">%2$s</xliff:g> fotografií"</string> + <string name="cache_done" msgid="9194449192869777483">"Preberanie bolo dokončené"</string> + <string name="albums" msgid="7320787705180057947">"Albumy"</string> + <string name="times" msgid="2023033894889499219">"Časy"</string> + <string name="locations" msgid="6649297994083130305">"Miesta"</string> + <string name="people" msgid="4114003823747292747">"Ľudia"</string> + <string name="tags" msgid="5539648765482935955">"Značky"</string> + <string name="group_by" msgid="4308299657902209357">"Zoskupiť podľa"</string> + <!-- no translation found for settings (1534847740615665736) --> + <skip /> + <string name="prefs_accounts" msgid="7942761992713671670">"Nastavenia účtu"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Nastavenia spotreby dát"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Automatické odovzdanie"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Ďalšie nastavenia"</string> + <string name="about_gallery" msgid="8667445445883757255">"O Galérii"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Synchronizovať len v sieti Wi-Fi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Automaticky odovzdať všetky zaznamenané fotografie a videá do súkromného webového albumu programu Picasa"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Povoliť automatické odovzdanie"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Synchr. fotiek Google: ZAPNUTÁ"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Synchr. fotografií Google: VYPNUTÁ"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Zmena predvolieb synchr. alebo odstr. účtu"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Zobraziť fotografie a videá z tohto účtu v Galérii"</string> + <string name="add_account" msgid="4271217504968243974">"Pridať účet"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Výber účtu na automat. odovzd."</string> +</resources> diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml new file mode 100644 index 000000000..47810abbf --- /dev/null +++ b/res/values-sl/strings.xml @@ -0,0 +1,173 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galerija"</string> + <string name="gadget_title" msgid="259405922673466798">"Okvir slike"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Videopredvajalnik"</string> + <string name="loading_video" msgid="4013492720121891585">"Nalaganje videoposnetka ..."</string> + <string name="loading_image" msgid="1200894415793838191">"Nalaganje slike ..."</string> + <string name="loading_account" msgid="928195413034552034">"Nalaganje računa???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Nadaljuj predvajanje videoposnetka"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Nadaljevanje predvajanja od %s?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Nadaljuj predvajanje"</string> + <string name="loading" msgid="7038208555304563571">"Prenos …"</string> + <string name="fail_to_load" msgid="2710120770735315683">"Nalaganje ni uspelo"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Ni sličice"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Začni znova"</string> + <string name="crop_save_text" msgid="8821167985419282305">"V redu"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Tapnite obraz, če želite začeti."</string> + <string name="saving_image" msgid="7270334453636349407">"Shranjevanje slike ..."</string> + <string name="crop_label" msgid="521114301871349328">"Obreži sliko"</string> + <string name="select_image" msgid="7841406150484742140">"Izberite fotogr."</string> + <string name="select_video" msgid="4859510992798615076">"Izberite videoposn."</string> + <string name="select_item" msgid="2257529413100472599">"Izberite elemente"</string> + <string name="select_album" msgid="4632641262236697235">"Izberite albume"</string> + <string name="select_group" msgid="9090385962030340391">"Izberite skupine"</string> + <string name="set_image" msgid="2331476809308010401">"Nastavi sliko kot"</string> + <string name="wallpaper" msgid="9222901738515471972">"Nastavljanje slike za ozadje, počakajte ..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Slika za ozadje"</string> + <string name="delete" msgid="2839695998251824487">"Izbriši"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Potrdite brisanje"</string> + <string name="cancel" msgid="3637516880917356226">"Prekliči"</string> + <string name="share" msgid="3619042788254195341">"Skupna raba"</string> + <string name="select_all" msgid="8623593677101437957">"Izberi vse"</string> + <string name="deselect_all" msgid="7397531298370285581">"Prekliči celoten izbor"</string> + <string name="slideshow" msgid="4355906903247112975">"Diaprojekcija"</string> + <string name="details" msgid="8415120088556445230">"Podrobnosti"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Preklopi na Fotoaparat"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"Pokaži na zemljevidu"</string> + <string name="rotate_left" msgid="7412075232752726934">"Zasukaj levo"</string> + <string name="rotate_right" msgid="7340681085011826618">"Zasukaj desno"</string> + <string name="no_such_item" msgid="3161074758669642065">"Elementa ni bilo mogoče najti"</string> + <string name="edit" msgid="1502273844748580847">"Urejanje"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Na voljo ni nobenega programa"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Zahteve za predpomnjenje procesa"</string> + <string name="caching_label" msgid="3244800874547101776">"Predpomnjenje ..."</string> + <string name="crop" msgid="7970750655414797277">"Obreži"</string> + <string name="set_as" msgid="3636764710790507868">"Nastavi kot"</string> + <string name="video_err" msgid="7917736494827857757">"Videoposnetka ni mogoče predvajati"</string> + <string name="group_by_location" msgid="316641628989023253">"Po lokaciji"</string> + <string name="group_by_time" msgid="9046168567717963573">"Po uri"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Po oznakah"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Po ljudeh"</string> + <string name="group_by_album" msgid="1532818636053818958">"Po albumu"</string> + <string name="group_by_size" msgid="153766174950394155">"Glede na velikost"</string> + <string name="untagged" msgid="7281481064509590402">"Neoznačeno"</string> + <string name="no_location" msgid="2036710947563713111">"Ni lokacije"</string> + <string name="show_images_only" msgid="7263218480867672653">"Samo slike"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Samo videoposnetki"</string> + <string name="show_all" msgid="4780647751652596980">"Slike in videoposnetki"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Fotogalerija"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Ni fotografij"</string> + <string name="crop_saved" msgid="4684933379430649946">"Obrezana slika je bila shranjena v prenos"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Obrezana slika ni shranjena"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Na voljo ni nobenega albuma"</string> + <string name="empty_album" msgid="6307897398825514762">"Na voljo ni slik/videoposnetkov"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Spletni albumi Picasa"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Omogoči dostop brez povezave"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Končano"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d od %2$d elementov:"</string> + <string name="title" msgid="7622928349908052569">"Naslov"</string> + <string name="description" msgid="3016729318096557520">"Opis"</string> + <string name="time" msgid="1367953006052876956">"Ura"</string> + <string name="location" msgid="3432705876921618314">"Lokacija"</string> + <string name="path" msgid="4725740395885105824">"Pot"</string> + <string name="width" msgid="9215847239714321097">"Širina"</string> + <string name="height" msgid="3648885449443787772">"Višina"</string> + <string name="orientation" msgid="4958327983165245513">"Usmerjenost"</string> + <string name="duration" msgid="8160058911218541616">"Trajanje"</string> + <string name="mimetype" msgid="3518268469266183548">"Vrsta MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Velikost datoteke"</string> + <string name="maker" msgid="7921835498034236197">"Izdelovalec"</string> + <string name="model" msgid="8240207064064337366">"Model"</string> + <string name="flash" msgid="2816779031261147723">"Bliskavica"</string> + <string name="aperture" msgid="5920657630303915195">"Zaslonka"</string> + <string name="focal_length" msgid="1291383769749877010">"Gorišč. razd."</string> + <string name="white_balance" msgid="8122534414851280901">"Ravn. beline"</string> + <string name="exposure_time" msgid="3146642210127439553">"Čas osvetlitve"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Ročno"</string> + <string name="auto" msgid="4296941368722892821">"Samod."</string> + <string name="flash_on" msgid="7891556231891837284">"Blis. sprožena"</string> + <string name="flash_off" msgid="1445443413822680010">"Brez bliskav."</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Priprava albuma, da bo na voljo brez povezave"</item> + <item quantity="other" msgid="6929905722448632886">"Priprava albumov, da bodo na voljo brez povezave"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Element je shranjen lokalno in na voljo brez povezave."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Vsi albumi"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Lokalni albumi"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"Naprave MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Albumi Picasa"</string> + <string name="free_space_format" msgid="8766337315709161215">"Prosto: <xliff:g id="BYTES">%s</xliff:g>"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> ali manj"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> ali več"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> do <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Uvozi"</string> + <string name="import_complete" msgid="1098450310074640619">"Uvoz je končan"</string> + <string name="import_fail" msgid="5205927625132482529">"Uvoz ni uspel"</string> + <string name="camera_connected" msgid="6984353643349303075">"Kamera je priključena"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Kamera je izklopljena"</string> + <string name="click_import" msgid="6407959065464291972">"Tapnite tukaj, če želite uvoziti"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Slike iz albuma"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Naključno razporedi vse slike"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Izberite sliko"</string> + <string name="widget_type" msgid="7308564524449340985">"Vrsta pripomočka"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Diaprojekcija"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Vnaprejšnji prenos fotografij iz Picase:"</string> + <string name="cache_status" msgid="7690438435538533106">"Prenos <xliff:g id="NUMBER_0">%1$s</xliff:g> od <xliff:g id="NUMBER_1">%2$s</xliff:g> fotografij"</string> + <string name="cache_done" msgid="9194449192869777483">"Prenos je končan"</string> + <string name="albums" msgid="7320787705180057947">"Albumi"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"Lokacije"</string> + <string name="people" msgid="4114003823747292747">"Osebe"</string> + <string name="tags" msgid="5539648765482935955">"Oznake"</string> + <string name="group_by" msgid="4308299657902209357">"Razvrsti po"</string> + <!-- no translation found for settings (1534847740615665736) --> + <skip /> + <string name="prefs_accounts" msgid="7942761992713671670">"Nastavitve računa"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Nastavitve uporabe podatkov"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Samodejni prenos"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Druge nastavitve"</string> + <string name="about_gallery" msgid="8667445445883757255">"O galeriji"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"SInhroniziraj samo v omrežju Wi-Fi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Samodejni prenos fotografij in videoposnetkov v zasebni spletni album Picasa"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Omogoči samodejni prenos"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Sinhr. Googlovih fot. je vkl."</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Sinhroniziranje Googlovih fotografij je izklopljeno"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Sprem. nast. sinhr. ali odstran. računa"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Ogled fotografij in videoposnetkov iz tega računa v galeriji"</string> + <string name="add_account" msgid="4271217504968243974">"Dodaj račun"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Izberite račun za samodejni prenos"</string> +</resources> diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml new file mode 100644 index 000000000..c490b161d --- /dev/null +++ b/res/values-sr/strings.xml @@ -0,0 +1,173 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Галерија"</string> + <string name="gadget_title" msgid="259405922673466798">"Оквир слике"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Видео плејер"</string> + <string name="loading_video" msgid="4013492720121891585">"Учитавање видео снимка…"</string> + <string name="loading_image" msgid="1200894415793838191">"Учитавање слике…"</string> + <string name="loading_account" msgid="928195413034552034">"Учитавање налога???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Наставак видео снимка"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Желите ли да наставите репродукцију од %s ?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Настави репродукцију"</string> + <string name="loading" msgid="7038208555304563571">"Учитавање…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"Учитавање није успело"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Нема сличице"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Започни поново"</string> + <string name="crop_save_text" msgid="8821167985419282305">"Потврди"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"За почетак додирните неко лице."</string> + <string name="saving_image" msgid="7270334453636349407">"Чување слике…"</string> + <string name="crop_label" msgid="521114301871349328">"Опсеци слику"</string> + <string name="select_image" msgid="7841406150484742140">"Избор фотографије"</string> + <string name="select_video" msgid="4859510992798615076">"Избор видео снимка"</string> + <string name="select_item" msgid="2257529413100472599">"Избор ставке(и)"</string> + <string name="select_album" msgid="4632641262236697235">"Избор албума"</string> + <string name="select_group" msgid="9090385962030340391">"Избор групе(а)"</string> + <string name="set_image" msgid="2331476809308010401">"Постављање слике као"</string> + <string name="wallpaper" msgid="9222901738515471972">"Постављање позадине. Сачекајте…"</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Позадина"</string> + <string name="delete" msgid="2839695998251824487">"Избриши"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Потврдите брисање"</string> + <string name="cancel" msgid="3637516880917356226">"Откажи"</string> + <string name="share" msgid="3619042788254195341">"Дели"</string> + <string name="select_all" msgid="8623593677101437957">"Изабери све"</string> + <string name="deselect_all" msgid="7397531298370285581">"Опозови све изборе"</string> + <string name="slideshow" msgid="4355906903247112975">"Пројекција слајдова"</string> + <string name="details" msgid="8415120088556445230">"Детаљи"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Пребацивање на Камеру"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"Прикажи на мапи"</string> + <string name="rotate_left" msgid="7412075232752726934">"Ротирај улево"</string> + <string name="rotate_right" msgid="7340681085011826618">"Ротирај удесно"</string> + <string name="no_such_item" msgid="3161074758669642065">"Ставка није пронађена"</string> + <string name="edit" msgid="1502273844748580847">"Измени"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Нема доступне апликације"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Захтеви за кеширање процеса"</string> + <string name="caching_label" msgid="3244800874547101776">"Кеширање..."</string> + <string name="crop" msgid="7970750655414797277">"Опсеци"</string> + <string name="set_as" msgid="3636764710790507868">"Постави као"</string> + <string name="video_err" msgid="7917736494827857757">"Није могуће пустити видео"</string> + <string name="group_by_location" msgid="316641628989023253">"Према локацији"</string> + <string name="group_by_time" msgid="9046168567717963573">"Према времену"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Према ознакама"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Према особама"</string> + <string name="group_by_album" msgid="1532818636053818958">"Према албуму"</string> + <string name="group_by_size" msgid="153766174950394155">"Према величини"</string> + <string name="untagged" msgid="7281481064509590402">"Није означено"</string> + <string name="no_location" msgid="2036710947563713111">"Без локације"</string> + <string name="show_images_only" msgid="7263218480867672653">"Само слике"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Само видео снимци"</string> + <string name="show_all" msgid="4780647751652596980">"Слике и видео снимци"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Фото-галерија"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Нема фотографија"</string> + <string name="crop_saved" msgid="4684933379430649946">"Исечена слика је сачувана у преузимањима"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Исечена слика није сачувана"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Нема доступних албума"</string> + <string name="empty_album" msgid="6307897398825514762">"Нема доступних слика/видео записа"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa Веб албуми"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Учини доступним ван мреже"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Done"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d од %2$d ставке(и):"</string> + <string name="title" msgid="7622928349908052569">"Наслов"</string> + <string name="description" msgid="3016729318096557520">"Опис"</string> + <string name="time" msgid="1367953006052876956">"Време"</string> + <string name="location" msgid="3432705876921618314">"Локација"</string> + <string name="path" msgid="4725740395885105824">"Путања"</string> + <string name="width" msgid="9215847239714321097">"Ширина"</string> + <string name="height" msgid="3648885449443787772">"Висина"</string> + <string name="orientation" msgid="4958327983165245513">"Положај"</string> + <string name="duration" msgid="8160058911218541616">"Трајање"</string> + <string name="mimetype" msgid="3518268469266183548">"MIME тип"</string> + <string name="file_size" msgid="4670384449129762138">"Вел. датотеке"</string> + <string name="maker" msgid="7921835498034236197">"Аутор"</string> + <string name="model" msgid="8240207064064337366">"Модел"</string> + <string name="flash" msgid="2816779031261147723">"Блиц"</string> + <string name="aperture" msgid="5920657630303915195">"Отвор бленде"</string> + <string name="focal_length" msgid="1291383769749877010">"Фокална дужина"</string> + <string name="white_balance" msgid="8122534414851280901">"Баланс беле"</string> + <string name="exposure_time" msgid="3146642210127439553">"Време експоз."</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"мм"</string> + <string name="manual" msgid="6608905477477607865">"Ручно"</string> + <string name="auto" msgid="4296941368722892821">"Аутом."</string> + <string name="flash_on" msgid="7891556231891837284">"Блиц је актив."</string> + <string name="flash_off" msgid="1445443413822680010">"Без блица"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Омогућавање доступности албума ван мреже"</item> + <item quantity="other" msgid="6929905722448632886">"Омогућавање доступности албума ван мреже"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Ова ставка је локално сачувана и доступна ван мреже."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Сви албуми"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Локални албуми"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"MTP уређаји"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Picasa албуми"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> слободно"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> или мање"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> или више"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> до <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Увези"</string> + <string name="import_complete" msgid="1098450310074640619">"Увоз је довршен"</string> + <string name="import_fail" msgid="5205927625132482529">"Увоз није успео"</string> + <string name="camera_connected" msgid="6984353643349303075">"Камера је прикључена"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Камера је искључена"</string> + <string name="click_import" msgid="6407959065464291972">"Додирните овде за увоз"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Слике из албума"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Пусти све слике насумично"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Изабери слику"</string> + <string name="widget_type" msgid="7308564524449340985">"Тип виџета"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Пројекција слајдова"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Учитавање picasa фотографија унапред:"</string> + <string name="cache_status" msgid="7690438435538533106">"Преузимање <xliff:g id="NUMBER_0">%1$s</xliff:g> од <xliff:g id="NUMBER_1">%2$s</xliff:g> фотографије(а)"</string> + <string name="cache_done" msgid="9194449192869777483">"Преузимање је завршено"</string> + <string name="albums" msgid="7320787705180057947">"Албуми"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"Локације"</string> + <string name="people" msgid="4114003823747292747">"Особе"</string> + <string name="tags" msgid="5539648765482935955">"Ознаке"</string> + <string name="group_by" msgid="4308299657902209357">"Групиши према"</string> + <!-- no translation found for settings (1534847740615665736) --> + <skip /> + <string name="prefs_accounts" msgid="7942761992713671670">"Подешавања налога"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Подешавања коришћења података"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Аутоматско отпремање"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Остала подешавања"</string> + <string name="about_gallery" msgid="8667445445883757255">"О Галерији"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Синхронизација само на WiFi-ју"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Аутоматско отпремање свих фотографија и видео снимака које снимите у приватни Picasa веб албум"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Омогућавање аутоматског отпремања"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Google фото синх. је УКЉУЧЕНА"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Google фото синх. је ИСКЉУЧЕНА"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Промена подешавања синх. или укл. налога"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Преглед фотографија и видео снимака са налога у Галерији"</string> + <string name="add_account" msgid="4271217504968243974">"Додавање налога"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Избор налога за аутом. отпр."</string> +</resources> diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml new file mode 100644 index 000000000..f927b305b --- /dev/null +++ b/res/values-sv/strings.xml @@ -0,0 +1,172 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galleri"</string> + <string name="gadget_title" msgid="259405922673466798">"Bildram"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Videospelare"</string> + <string name="loading_video" msgid="4013492720121891585">"Läser in video…"</string> + <string name="loading_image" msgid="1200894415793838191">"Läser in bild..."</string> + <string name="loading_account" msgid="928195413034552034">"Läses kontot in???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Fortsätt spela videon"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Fortsätt spela upp från %s?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Fortsätt spela upp"</string> + <string name="loading" msgid="7038208555304563571">"Läser in..."</string> + <string name="fail_to_load" msgid="2710120770735315683">"Hämtningen misslyckades"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Ingen miniatyrbild"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Börja om"</string> + <string name="crop_save_text" msgid="8821167985419282305">"OK"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Knacka lätt på ett ansikte när du vill börja."</string> + <string name="saving_image" msgid="7270334453636349407">"Sparar bild…"</string> + <string name="crop_label" msgid="521114301871349328">"Beskär bild"</string> + <string name="select_image" msgid="7841406150484742140">"Välj en bild"</string> + <string name="select_video" msgid="4859510992798615076">"Välj ett videoklipp"</string> + <string name="select_item" msgid="2257529413100472599">"Välj objekt"</string> + <string name="select_album" msgid="4632641262236697235">"Välj album"</string> + <string name="select_group" msgid="9090385962030340391">"Välj grupp(er)"</string> + <string name="set_image" msgid="2331476809308010401">"Använd bild som"</string> + <string name="wallpaper" msgid="9222901738515471972">"Anger bakgrund, vänta…"</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Bakgrund"</string> + <string name="delete" msgid="2839695998251824487">"Ta bort"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Bekräfta borttagning"</string> + <string name="cancel" msgid="3637516880917356226">"Avbryt"</string> + <string name="share" msgid="3619042788254195341">"Dela"</string> + <string name="select_all" msgid="8623593677101437957">"Markera alla"</string> + <string name="deselect_all" msgid="7397531298370285581">"Avmarkera alla"</string> + <string name="slideshow" msgid="4355906903247112975">"Bildspel"</string> + <string name="details" msgid="8415120088556445230">"Information"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Byt till kamera"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"Visa på karta"</string> + <string name="rotate_left" msgid="7412075232752726934">"Rotera åt vänster"</string> + <string name="rotate_right" msgid="7340681085011826618">"Rotera åt höger"</string> + <string name="no_such_item" msgid="3161074758669642065">"Det gick inte att hitta objektet"</string> + <string name="edit" msgid="1502273844748580847">"Redigera"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Det finns inga tillgängliga appar"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Begäran om cachelagring bearbetas"</string> + <string name="caching_label" msgid="3244800874547101776">"Cachelagrar..."</string> + <string name="crop" msgid="7970750655414797277">"Beskär"</string> + <string name="set_as" msgid="3636764710790507868">"Använd som"</string> + <string name="video_err" msgid="7917736494827857757">"Det gick inte att spela videon"</string> + <string name="group_by_location" msgid="316641628989023253">"Efter plats"</string> + <string name="group_by_time" msgid="9046168567717963573">"Efter tid"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Efter taggar"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Efter personer"</string> + <string name="group_by_album" msgid="1532818636053818958">"Efter album"</string> + <string name="group_by_size" msgid="153766174950394155">"Efter storlek"</string> + <string name="untagged" msgid="7281481064509590402">"Saknar etikett"</string> + <string name="no_location" msgid="2036710947563713111">"Ingen plats"</string> + <string name="show_images_only" msgid="7263218480867672653">"Endast bilder"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Endast video"</string> + <string name="show_all" msgid="4780647751652596980">"Bilder och videor"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Fotogalleri"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Det finns inga bilder."</string> + <string name="crop_saved" msgid="4684933379430649946">"Den beskurna bilden har sparats i hämtade"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Den beskurna bilden sparas inte"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Det finns inte några tillgängliga album"</string> + <string name="empty_album" msgid="6307897398825514762">"Det finns inga tillgängliga bilder/videoklipp"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa webbalbum"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Gör tillgängliga offline"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Klar"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d av %2$d objekt:"</string> + <string name="title" msgid="7622928349908052569">"Titel"</string> + <string name="description" msgid="3016729318096557520">"Beskrivning"</string> + <string name="time" msgid="1367953006052876956">"Tid"</string> + <string name="location" msgid="3432705876921618314">"Plats"</string> + <string name="path" msgid="4725740395885105824">"Sökväg"</string> + <string name="width" msgid="9215847239714321097">"Bredd"</string> + <string name="height" msgid="3648885449443787772">"Höjd"</string> + <string name="orientation" msgid="4958327983165245513">"Riktning"</string> + <string name="duration" msgid="8160058911218541616">"Varaktighet"</string> + <string name="mimetype" msgid="3518268469266183548">"MIME-typ"</string> + <string name="file_size" msgid="4670384449129762138">"Filstorlek"</string> + <string name="maker" msgid="7921835498034236197">"Upphovsman"</string> + <string name="model" msgid="8240207064064337366">"Modell"</string> + <string name="flash" msgid="2816779031261147723">"Flash"</string> + <string name="aperture" msgid="5920657630303915195">"Bländare"</string> + <string name="focal_length" msgid="1291383769749877010">"Fokuslängd"</string> + <string name="white_balance" msgid="8122534414851280901">"Vitbalans"</string> + <string name="exposure_time" msgid="3146642210127439553">"Exponeringstid"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Manuell"</string> + <string name="auto" msgid="4296941368722892821">"Auto"</string> + <string name="flash_on" msgid="7891556231891837284">"Blixt utlöst"</string> + <string name="flash_off" msgid="1445443413822680010">"Ingen blixt"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Gör album tillgängligt offline"</item> + <item quantity="other" msgid="6929905722448632886">"Gör album tillgängliga offline"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Objektet lagras lokalt och är tillgängligt offline."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Alla album"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Lokala album"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"MTP-enheter"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Picasa-album"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> ledigt"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> eller mindre"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> eller mer"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> till <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Importera"</string> + <string name="import_complete" msgid="1098450310074640619">"Importen slutförd"</string> + <string name="import_fail" msgid="5205927625132482529">"Importen misslyckades"</string> + <string name="camera_connected" msgid="6984353643349303075">"Kameran är ansluten"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Kameran är inte ansluten"</string> + <string name="click_import" msgid="6407959065464291972">"Tryck här om du vill importera"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Bilder från ett album"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Blanda alla bilder"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Välj en bild"</string> + <string name="widget_type" msgid="7308564524449340985">"Widgettyp"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Bildspel"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Bilder från Picasa förhandshämtas:"</string> + <string name="cache_status" msgid="7690438435538533106">"Hämta <xliff:g id="NUMBER_0">%1$s</xliff:g> av <xliff:g id="NUMBER_1">%2$s</xliff:g> bilder"</string> + <string name="cache_done" msgid="9194449192869777483">"Hämtningen har slutförts"</string> + <string name="albums" msgid="7320787705180057947">"Album"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"Platser"</string> + <string name="people" msgid="4114003823747292747">"Personer"</string> + <string name="tags" msgid="5539648765482935955">"Taggar"</string> + <string name="group_by" msgid="4308299657902209357">"Ordna efter"</string> + <string name="settings" msgid="1534847740615665736">"Inställningar"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"Kontoinställningar"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Inställningar för dataanvändning"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Automatisk överföring"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Andra inställningar"</string> + <string name="about_gallery" msgid="8667445445883757255">"Om galleriet"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Synkronisera bara i Wi-Fi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Lägg automatiskt upp alla foton och videoklipp du spelar in i ett privat Picasa-webbalbum"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Aktivera automatisk överföring"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Google fotosynk är PÅ"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Google fotosynk är AV"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Ändra synkinst. eller ta bort kontot"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Visa foton och videor från det här kontot i Galleriet"</string> + <string name="add_account" msgid="4271217504968243974">"Lägg till konto"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Välj konto för autoöverföring"</string> +</resources> diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml new file mode 100644 index 000000000..ffed49d8f --- /dev/null +++ b/res/values-sw/strings.xml @@ -0,0 +1,233 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Matunzio"</string> + <string name="gadget_title" msgid="259405922673466798">"Fremu ya picha"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <!-- outdated translation 3697303290960009886 --> <string name="movie_view_label" msgid="3526526872644898229">"Filamu"</string> + <string name="loading_video" msgid="4013492720121891585">"Inapakia video..."</string> + <string name="loading_image" msgid="1200894415793838191">"Inapakia picha…"</string> + <!-- no translation found for loading_account (928195413034552034) --> + <skip /> + <string name="resume_playing_title" msgid="8996677350649355013">"Endelea na video"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Endelea kucheza kutoka %s?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Endelea kucheza"</string> + <string name="loading" msgid="7038208555304563571">"Inapakia…"</string> + <!-- outdated translation 3355969119388837437 --> <string name="fail_to_load" msgid="2710120770735315683">"Imeshindwa kupakia"</string> + <!-- no translation found for no_thumbnail (284723185546429750) --> + <skip /> + <string name="resume_playing_restart" msgid="5471008499835769292">"Anza tena"</string> + <string name="crop_save_text" msgid="8821167985419282305">"Sawa"</string> + <!-- no translation found for multiface_crop_help (3127018992717032779) --> + <skip /> + <string name="saving_image" msgid="7270334453636349407">"Inahifadhi picha…"</string> + <!-- no translation found for crop_label (521114301871349328) --> + <skip /> + <string name="select_image" msgid="7841406150484742140">"Chagua picha"</string> + <string name="select_video" msgid="4859510992798615076">"Chagua video"</string> + <string name="select_item" msgid="2257529413100472599">"Chagua vipengee"</string> + <string name="select_album" msgid="4632641262236697235">"Chagua albamu"</string> + <string name="select_group" msgid="9090385962030340391">"Chagua vikundi"</string> + <string name="set_image" msgid="2331476809308010401">"Weka picha kama"</string> + <!-- no translation found for wallpaper (9222901738515471972) --> + <skip /> + <!-- no translation found for camera_setas_wallpaper (797463183863414289) --> + <skip /> + <!-- no translation found for delete (2839695998251824487) --> + <skip /> + <string name="confirm_delete" msgid="5731757674837098707">"Thibitisha Kufuta"</string> + <!-- no translation found for cancel (3637516880917356226) --> + <skip /> + <string name="share" msgid="3619042788254195341">"Shiriki"</string> + <string name="select_all" msgid="8623593677101437957">"Chagua Zote"</string> + <string name="deselect_all" msgid="7397531298370285581">"Ghairi Zote Zilizochaguliwa"</string> + <string name="slideshow" msgid="4355906903247112975">"Onyesho la slaidi"</string> + <!-- no translation found for details (8415120088556445230) --> + <skip /> + <!-- no translation found for switch_to_camera (7280111806675169992) --> + <skip /> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"% 1 $ d imechaguliwa"</item> + <item quantity="one" msgid="2478365152745637768">"% 1 $ d imechaguliwa"</item> + <item quantity="other" msgid="754722656147810487">"% 1 $ d imechaguliwa"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"% 1 $ d imechaguliwa"</item> + <item quantity="one" msgid="6184377003099987825">"% 1 $ d imechaguliwa"</item> + <item quantity="other" msgid="53105607141906130">"% 1 $ d imechaguliwa"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"% 1 $ d imechaguliwa"</item> + <item quantity="one" msgid="5030162638216034260">"% 1 $ d imechaguliwa"</item> + <item quantity="other" msgid="3512041363942842738">"% 1 $ d imechaguliwa"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"Onyesha kwenye ramani"</string> + <string name="rotate_left" msgid="7412075232752726934">"Zungusha Kushoto"</string> + <string name="rotate_right" msgid="7340681085011826618">"Zungusha Kulia"</string> + <string name="no_such_item" msgid="3161074758669642065">"Kipengee hakikupatikana"</string> + <!-- no translation found for edit (1502273844748580847) --> + <skip /> + <string name="activity_not_found" msgid="3731390759313019518">"Hakuna programu inayopatikana"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Maombi ya Kuakibisha Mchakato"</string> + <string name="caching_label" msgid="3244800874547101776">"Inaakibisha..."</string> + <string name="crop" msgid="7970750655414797277">"Kata"</string> + <string name="set_as" msgid="3636764710790507868">"Weka kama"</string> + <string name="video_err" msgid="7917736494827857757">"Haiwezi kucheza video"</string> + <string name="group_by_location" msgid="316641628989023253">"Kwa mahali"</string> + <string name="group_by_time" msgid="9046168567717963573">"Kwa saa"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Kwa lebo"</string> + <!-- no translation found for group_by_faces (1566351636227274906) --> + <skip /> + <string name="group_by_album" msgid="1532818636053818958">"Kwa albamu"</string> + <!-- no translation found for group_by_size (153766174950394155) --> + <skip /> + <string name="untagged" msgid="7281481064509590402">"Ondoa lebo"</string> + <string name="no_location" msgid="2036710947563713111">"Hakuna Mahali"</string> + <string name="show_images_only" msgid="7263218480867672653">"Picha tu"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Video tu"</string> + <string name="show_all" msgid="4780647751652596980">"Picha na video"</string> + <!-- no translation found for appwidget_title (6410561146863700411) --> + <skip /> + <!-- no translation found for appwidget_empty_text (4123016777080388680) --> + <skip /> + <string name="crop_saved" msgid="4684933379430649946">"Picha iliyopunguzwa imehifadhiwa katika vipakuzi"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Picha iliyopunguzwa haijahifadhiwa"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Hakuna albamu zinazopatikana"</string> + <string name="empty_album" msgid="6307897398825514762">"Hakuna picha/video zinazopatikana"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Albamu Wavuti za Picasa"</string> + <!-- no translation found for picasa_posts (1055151689217481993) --> + <skip /> + <string name="make_available_offline" msgid="5157950985488297112">"Fanya ipatikane nje ya mkondo"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Kwisha"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"Vipengee %1$d kati ya %2$d:"</string> + <string name="title" msgid="7622928349908052569">"Kichwa"</string> + <string name="description" msgid="3016729318096557520">"Maelezo"</string> + <string name="time" msgid="1367953006052876956">"Saa"</string> + <string name="location" msgid="3432705876921618314">"Mahali"</string> + <string name="path" msgid="4725740395885105824">"Njia"</string> + <string name="width" msgid="9215847239714321097">"Upana"</string> + <string name="height" msgid="3648885449443787772">"Urefu"</string> + <string name="orientation" msgid="4958327983165245513">"Uelekezo"</string> + <string name="duration" msgid="8160058911218541616">"Muda"</string> + <string name="mimetype" msgid="3518268469266183548">"Aina ya MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Ukubwa wa Faili"</string> + <string name="maker" msgid="7921835498034236197">"Mtengenezaji"</string> + <string name="model" msgid="8240207064064337366">"Mtindo"</string> + <string name="flash" msgid="2816779031261147723">"Mmweko"</string> + <string name="aperture" msgid="5920657630303915195">"Kilango"</string> + <string name="focal_length" msgid="1291383769749877010">"Urefu wa Lengo"</string> + <string name="white_balance" msgid="8122534414851280901">"Usawazishaji wa Weupe"</string> + <string name="exposure_time" msgid="3146642210127439553">"Muda wa Mfichuo"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Mwongozo"</string> + <string name="auto" msgid="4296941368722892821">"Kiotomatiki"</string> + <string name="flash_on" msgid="7891556231891837284">"Mmweko umeanzishwa"</string> + <string name="flash_off" msgid="1445443413822680010">"Hakuna flash"</string> + <!-- no translation found for make_albums_available_offline:one (2955975726887896888) --> + <!-- no translation found for make_albums_available_offline:other (6929905722448632886) --> + <!-- no translation found for try_to_set_local_album_available_offline (2184754031896160755) --> + <skip /> + <!-- no translation found for set_label_all_albums (3507256844918130594) --> + <skip /> + <!-- no translation found for set_label_local_albums (5227548825039781) --> + <skip /> + <!-- no translation found for set_label_mtp_devices (5779788799122828528) --> + <skip /> + <!-- no translation found for set_label_picasa_albums (2736308697306982589) --> + <skip /> + <!-- no translation found for free_space_format (8766337315709161215) --> + <skip /> + <!-- no translation found for size_below (2074956730721942260) --> + <skip /> + <!-- no translation found for size_above (5324398253474104087) --> + <skip /> + <!-- no translation found for size_between (8779660840898917208) --> + <skip /> + <!-- no translation found for Import (3985447518557474672) --> + <skip /> + <!-- no translation found for import_complete (1098450310074640619) --> + <skip /> + <!-- no translation found for import_fail (5205927625132482529) --> + <skip /> + <!-- no translation found for camera_connected (6984353643349303075) --> + <skip /> + <!-- no translation found for camera_disconnected (3683036560562699311) --> + <skip /> + <!-- no translation found for click_import (6407959065464291972) --> + <skip /> + <!-- no translation found for widget_type_album (3245149644830731121) --> + <skip /> + <!-- no translation found for widget_type_shuffle (8594622705019763768) --> + <skip /> + <!-- no translation found for widget_type_photo (8384174698965738770) --> + <skip /> + <!-- no translation found for widget_type (7308564524449340985) --> + <skip /> + <!-- no translation found for slideshow_dream_name (6915963319933437083) --> + <skip /> + <!-- no translation found for cache_status_title (8414708919928621485) --> + <skip /> + <!-- no translation found for cache_status (7690438435538533106) --> + <skip /> + <!-- no translation found for cache_done (9194449192869777483) --> + <skip /> + <!-- no translation found for albums (7320787705180057947) --> + <skip /> + <string name="times" msgid="2023033894889499219">"Nyakati"</string> + <!-- no translation found for locations (6649297994083130305) --> + <skip /> + <!-- no translation found for people (4114003823747292747) --> + <skip /> + <!-- no translation found for tags (5539648765482935955) --> + <skip /> + <string name="group_by" msgid="4308299657902209357">"Panga kwa kikundi na"</string> + <!-- no translation found for settings (1534847740615665736) --> + <skip /> + <!-- no translation found for prefs_accounts (7942761992713671670) --> + <skip /> + <!-- no translation found for prefs_data_usage (410592732727343215) --> + <skip /> + <!-- no translation found for prefs_auto_upload (2467627128066665126) --> + <skip /> + <!-- no translation found for prefs_other_settings (6034181851440646681) --> + <skip /> + <!-- no translation found for about_gallery (8667445445883757255) --> + <skip /> + <!-- no translation found for sync_on_wifi_only (5795753226259399958) --> + <skip /> + <!-- no translation found for helptext_auto_upload (133741242503097377) --> + <skip /> + <!-- no translation found for enable_auto_upload (1586329406342131) --> + <skip /> + <!-- no translation found for photo_sync_is_on (1653898269297050634) --> + <skip /> + <!-- no translation found for photo_sync_is_off (6464193461664544289) --> + <skip /> + <!-- no translation found for helptext_photo_sync (8617245939103545623) --> + <skip /> + <!-- no translation found for view_photo_for_account (5608040380422337939) --> + <skip /> + <!-- no translation found for add_account (4271217504968243974) --> + <skip /> + <!-- no translation found for auto_upload_chooser_title (1494524693870792948) --> + <skip /> +</resources> diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml new file mode 100644 index 000000000..d080488a1 --- /dev/null +++ b/res/values-th/strings.xml @@ -0,0 +1,177 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"แกลเลอรี"</string> + <string name="gadget_title" msgid="259405922673466798">"กรอบภาพ"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"โปรแกรมเล่นวิดีโอ"</string> + <string name="loading_video" msgid="4013492720121891585">"กำลังโหลดวิดีโอ..."</string> + <string name="loading_image" msgid="1200894415793838191">"กำลังโหลดภาพ..."</string> + <string name="loading_account" msgid="928195413034552034">"กำลังโหลดบัญชี???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"เล่นวิดีโอต่อ"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"ต้องการเล่นต่อจาก %s หรือไม่"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"เล่นต่อ"</string> + <string name="loading" msgid="7038208555304563571">"กำลังโหลด…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"การโหลดล้มเหลว"</string> + <string name="no_thumbnail" msgid="284723185546429750">"ไม่มีภาพขนาดย่อ"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"เริ่มต้นใหม่"</string> + <string name="crop_save_text" msgid="8821167985419282305">"ตกลง"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"แตะที่ใบหน้าเพื่อเริ่ม"</string> + <string name="saving_image" msgid="7270334453636349407">"กำลังบันทึกภาพ..."</string> + <string name="crop_label" msgid="521114301871349328">"ตัดภาพ"</string> + <string name="select_image" msgid="7841406150484742140">"เลือกรูปภาพ"</string> + <string name="select_video" msgid="4859510992798615076">"เลือกวิดีโอ"</string> + <string name="select_item" msgid="2257529413100472599">"เลือกรายการ"</string> + <string name="select_album" msgid="4632641262236697235">"เลือกอัลบั้ม"</string> + <string name="select_group" msgid="9090385962030340391">"เลือกกลุ่ม"</string> + <string name="set_image" msgid="2331476809308010401">"ตั้งค่าภาพเป็น"</string> + <string name="wallpaper" msgid="9222901738515471972">"กำลังตั้งค่าวอลเปเปอร์ โปรดรอสักครู่..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"วอลเปเปอร์"</string> + <string name="delete" msgid="2839695998251824487">"ลบ"</string> + <string name="confirm_delete" msgid="5731757674837098707">"ยืนยันการลบ"</string> + <string name="cancel" msgid="3637516880917356226">"ยกเลิก"</string> + <string name="share" msgid="3619042788254195341">"แบ่งปัน"</string> + <string name="select_all" msgid="8623593677101437957">"เลือกทั้งหมด"</string> + <string name="deselect_all" msgid="7397531298370285581">"ยกเลิกการเลือกทั้งหมด"</string> + <string name="slideshow" msgid="4355906903247112975">"การนำเสนอภาพนิ่ง"</string> + <string name="details" msgid="8415120088556445230">"รายละเอียด"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"สลับเป็นกล้องถ่ายรูป"</string> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"เลือกไว้ %1$d รายการ"</item> + <item quantity="one" msgid="2478365152745637768">"เลือกไว้ %1$d รายการ"</item> + <item quantity="other" msgid="754722656147810487">"เลือกไว้ %1$d รายการ"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"เลือกไว้ %1$d รายการ"</item> + <item quantity="one" msgid="6184377003099987825">"เลือกไว้ %1$d รายการ"</item> + <item quantity="other" msgid="53105607141906130">"เลือกไว้ %1$d รายการ"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"เลือกไว้ %1$d รายการ"</item> + <item quantity="one" msgid="5030162638216034260">"เลือกไว้ %1$d รายการ"</item> + <item quantity="other" msgid="3512041363942842738">"เลือกไว้ %1$d รายการ"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"แสดงบนแผนที่"</string> + <string name="rotate_left" msgid="7412075232752726934">"หมุนไปทางซ้าย"</string> + <string name="rotate_right" msgid="7340681085011826618">"หมุนไปทางขวา"</string> + <string name="no_such_item" msgid="3161074758669642065">"ไม่พบรายการ"</string> + <string name="edit" msgid="1502273844748580847">"แก้ไข"</string> + <string name="activity_not_found" msgid="3731390759313019518">"ไม่มีแอปพลิเคชันที่ใช้ได้"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"ประมวลผลคำขอแคช"</string> + <string name="caching_label" msgid="3244800874547101776">"กำลังแคช..."</string> + <string name="crop" msgid="7970750655414797277">"ตัดภาพ"</string> + <string name="set_as" msgid="3636764710790507868">"ตั้งค่าเป็น"</string> + <string name="video_err" msgid="7917736494827857757">"เล่นวิดีโอไม่ได้"</string> + <string name="group_by_location" msgid="316641628989023253">"ตามตำแหน่ง"</string> + <string name="group_by_time" msgid="9046168567717963573">"ตามเวลา"</string> + <string name="group_by_tags" msgid="3568731317210676160">"ตามแท็ก"</string> + <string name="group_by_faces" msgid="1566351636227274906">"จัดกลุ่มตามใบหน้าบุคคล"</string> + <string name="group_by_album" msgid="1532818636053818958">"ตามอัลบั้ม"</string> + <string name="group_by_size" msgid="153766174950394155">"ตามขนาด"</string> + <string name="untagged" msgid="7281481064509590402">"ยกเลิกการติดแท็ก"</string> + <string name="no_location" msgid="2036710947563713111">"ไม่มีข้อมูลสถานที่"</string> + <string name="show_images_only" msgid="7263218480867672653">"เฉพาะภาพ"</string> + <string name="show_videos_only" msgid="3850394623678871697">"เฉพาะวิดีโอ"</string> + <string name="show_all" msgid="4780647751652596980">"ภาพและวิดีโอ"</string> + <string name="appwidget_title" msgid="6410561146863700411">"แกลเลอรีรูปภาพ"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"ไม่มีรูปภาพ"</string> + <string name="crop_saved" msgid="4684933379430649946">"บันทึกภาพที่ครอบตัดไว้ในดาวน์โหลดแล้ว"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"ไม่บันทึกภาพที่ครอบตัด"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"ไม่มีอัลบั้มที่ใช้ได้"</string> + <string name="empty_album" msgid="6307897398825514762">"ไม่มีภาพ/วิดีโอที่ใช้ได้"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa Web Albums"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"ทำให้ใช้งานได้แบบออฟไลน์"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"เสร็จสิ้น"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d จาก %2$d รายการ:"</string> + <string name="title" msgid="7622928349908052569">"ชื่อ"</string> + <string name="description" msgid="3016729318096557520">"คำอธิบาย"</string> + <string name="time" msgid="1367953006052876956">"เวลา"</string> + <string name="location" msgid="3432705876921618314">"สถานที่"</string> + <string name="path" msgid="4725740395885105824">"เส้นทาง"</string> + <string name="width" msgid="9215847239714321097">"ความกว้าง"</string> + <string name="height" msgid="3648885449443787772">"ความสูง"</string> + <string name="orientation" msgid="4958327983165245513">"การวางแนว"</string> + <string name="duration" msgid="8160058911218541616">"ระยะเวลา"</string> + <string name="mimetype" msgid="3518268469266183548">"ประเภท MIME"</string> + <string name="file_size" msgid="4670384449129762138">"ขนาดไฟล์"</string> + <string name="maker" msgid="7921835498034236197">"ยี่ห้อ"</string> + <string name="model" msgid="8240207064064337366">"รุ่น"</string> + <string name="flash" msgid="2816779031261147723">"แฟลช"</string> + <string name="aperture" msgid="5920657630303915195">"รูรับแสง"</string> + <string name="focal_length" msgid="1291383769749877010">"ระยะโฟกัส"</string> + <string name="white_balance" msgid="8122534414851280901">"ไวท์บาลานซ์"</string> + <string name="exposure_time" msgid="3146642210127439553">"เวลาเปิดรับแสง"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"มม."</string> + <string name="manual" msgid="6608905477477607865">"ปรับเอง"</string> + <string name="auto" msgid="4296941368722892821">"อัตโนมัติ"</string> + <string name="flash_on" msgid="7891556231891837284">"แฟลชทำงาน"</string> + <string name="flash_off" msgid="1445443413822680010">"ไม่เปิดแฟลช"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"กำลังทำให้ใช้งานอัลบั้มแบบออฟไลน์ได้"</item> + <item quantity="other" msgid="6929905722448632886">"กำลังทำให้ใช้งานอัลบั้มแบบออฟไลน์ได้"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"รายการนี้จัดเก็บภายในเครื่องและสามารถใช้งานแบบออฟไลน์"</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"อัลบั้มทั้งหมด"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"อัลบั้มในเครื่อง"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"อุปกรณ์ MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Picasa Albums"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> ว่าง"</string> + <string name="size_below" msgid="2074956730721942260">"ไม่เกิน <xliff:g id="SIZE">%1$s</xliff:g>"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> ขึ้นไป"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> ถึง <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"นำเข้า"</string> + <string name="import_complete" msgid="1098450310074640619">"นำเข้าเสร็จสมบูรณ์"</string> + <string name="import_fail" msgid="5205927625132482529">"นำเข้าไม่สำเร็จ"</string> + <string name="camera_connected" msgid="6984353643349303075">"เชื่อมต่อกล้องถ่ายรูปแล้ว"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"ตัดการเชื่อมต่อกล้องถ่ายรูป"</string> + <string name="click_import" msgid="6407959065464291972">"แตะที่นี่เพื่อนำเข้า"</string> + <string name="widget_type_album" msgid="3245149644830731121">"รูปภาพจากอัลบั้ม"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"สุ่มภาพทั้งหมด"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"เลือกภาพ"</string> + <string name="widget_type" msgid="7308564524449340985">"ประเภทวิดเจ็ต"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"สไลด์โชว์"</string> + <string name="cache_status_title" msgid="8414708919928621485">"กำลังโหลดภาพถ่าย Picasa ล่วงหน้า:"</string> + <string name="cache_status" msgid="7690438435538533106">"ดาวน์โหลดภาพถ่าย <xliff:g id="NUMBER_0">%1$s</xliff:g> จาก <xliff:g id="NUMBER_1">%2$s</xliff:g> ภาพ"</string> + <string name="cache_done" msgid="9194449192869777483">"การดาวน์โหลดเสร็จสมบูรณ์"</string> + <string name="albums" msgid="7320787705180057947">"อัลบั้ม"</string> + <string name="times" msgid="2023033894889499219">"เวลา"</string> + <string name="locations" msgid="6649297994083130305">"ตำแหน่ง"</string> + <string name="people" msgid="4114003823747292747">"ผู้คน"</string> + <string name="tags" msgid="5539648765482935955">"แท็ก"</string> + <string name="group_by" msgid="4308299657902209357">"จัดกลุ่มตาม"</string> + <string name="settings" msgid="1534847740615665736">"การตั้งค่า"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"การตั้งค่าบัญชี"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"การตั้งค่าการใช้ข้อมูล"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"อัปโหลดอัตโนมัติ"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"การตั้งค่าอื่นๆ"</string> + <string name="about_gallery" msgid="8667445445883757255">"เกี่ยวกับแกลเลอรี"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"ซิงค์เมื่อใช้ Wi-Fi เท่านั้น"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"อัปโหลดรูปภาพและวิดีโอทั้งหมดที่คุณถ่ายไปยัง Picasa Web Albums โดยอัตโนมัติ"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"เปิดใช้งานการอัปโหลดอัตโนมัติ"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"การซิงค์ Google Photos เปิด"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"การซิงค์ Google Photos ปิด"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"เปลี่ยนค่ากำหนดการซิงค์หรือนำบัญชีนี้ออก"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"ดูรูปภาพและวิดีโอจากบัญชีนี้ในแกลเลอรี"</string> + <string name="add_account" msgid="4271217504968243974">"เพิ่มบัญชี"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"เลือกบัญชีอัปโหลดอัตโนมัติ"</string> +</resources> diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml new file mode 100644 index 000000000..a497f8820 --- /dev/null +++ b/res/values-tl/strings.xml @@ -0,0 +1,178 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Gallery"</string> + <string name="gadget_title" msgid="259405922673466798">"Frame ng larawan"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Player ng video"</string> + <string name="loading_video" msgid="4013492720121891585">"Naglo-load ng video…"</string> + <string name="loading_image" msgid="1200894415793838191">"Nilo-load ang larawan…"</string> + <string name="loading_account" msgid="928195413034552034">"Nilo-load ang account???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Ipagpatuloy ang video"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Ipagpatuloy ang pag-play mula sa %s ?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Ipagpatuloy ang pag-play"</string> + <string name="loading" msgid="7038208555304563571">"Naglo-load…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"Nabigong ma-load"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Walang thumbnail"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Magsimula na"</string> + <string name="crop_save_text" msgid="8821167985419282305">"Ok"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Tumapik ng mukha upang magsimula."</string> + <string name="saving_image" msgid="7270334453636349407">"Nagse-save ng larawan..."</string> + <string name="crop_label" msgid="521114301871349328">"I-crop ang larawan"</string> + <string name="select_image" msgid="7841406150484742140">"Pumili ng larawan"</string> + <string name="select_video" msgid="4859510992798615076">"Pumili ng video"</string> + <string name="select_item" msgid="2257529413100472599">"Pumili ng (mga) item"</string> + <string name="select_album" msgid="4632641262236697235">"Pumili ng (mga) album"</string> + <string name="select_group" msgid="9090385962030340391">"Pumili ng (mga) pangkat"</string> + <string name="set_image" msgid="2331476809308010401">"Itakda ang larawan bilang"</string> + <string name="wallpaper" msgid="9222901738515471972">"Nagtatakda ng wallpaper, pakihintay..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Wallpaper"</string> + <string name="delete" msgid="2839695998251824487">"Tanggalin"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Kumpirmahin ang Pagtanggal"</string> + <string name="cancel" msgid="3637516880917356226">"Kanselahin"</string> + <string name="share" msgid="3619042788254195341">"Ibahagi"</string> + <string name="select_all" msgid="8623593677101437957">"Piliin Lahat"</string> + <string name="deselect_all" msgid="7397531298370285581">"Alisin sa Pagkakapili Lahat"</string> + <string name="slideshow" msgid="4355906903247112975">"Slideshow"</string> + <string name="details" msgid="8415120088556445230">"Mga Detalye"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Lumipat sa Camera"</string> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"%1$d ang napili"</item> + <item quantity="one" msgid="2478365152745637768">"%1$d ang napili"</item> + <item quantity="other" msgid="754722656147810487">"%1$d ang napili"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"%1$d ang napili"</item> + <item quantity="one" msgid="6184377003099987825">"%1$d ang napili"</item> + <item quantity="other" msgid="53105607141906130">"%1$d ang napili"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"%1$d ang napili"</item> + <item quantity="one" msgid="5030162638216034260">"%1$d ang napili"</item> + <item quantity="other" msgid="3512041363942842738">"%1$d ang napili"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"Ipakita sa mapa"</string> + <string name="rotate_left" msgid="7412075232752726934">"I-rotate Pakaliwa"</string> + <string name="rotate_right" msgid="7340681085011826618">"I-rotate Pakanan"</string> + <string name="no_such_item" msgid="3161074758669642065">"Hindi nahanap ang item"</string> + <string name="edit" msgid="1502273844748580847">"I-edit"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Walang available na application"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Mga Kahilingan na Pag-cache ng Proseso"</string> + <string name="caching_label" msgid="3244800874547101776">"Caching..."</string> + <string name="crop" msgid="7970750655414797277">"I-crop"</string> + <string name="set_as" msgid="3636764710790507868">"Itakda bilang"</string> + <string name="video_err" msgid="7917736494827857757">"Hindi ma-play ang video"</string> + <string name="group_by_location" msgid="316641628989023253">"Ayon sa lokasyon"</string> + <string name="group_by_time" msgid="9046168567717963573">"Ayon sa oras"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Ayon sa mga tag"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Ayon sa mga tao"</string> + <string name="group_by_album" msgid="1532818636053818958">"Ayon sa album"</string> + <string name="group_by_size" msgid="153766174950394155">"Ayon sa laki"</string> + <string name="untagged" msgid="7281481064509590402">"Hindi naka-tag"</string> + <string name="no_location" msgid="2036710947563713111">"Walang Lokasyon"</string> + <string name="show_images_only" msgid="7263218480867672653">"Mga larawan lamang"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Mga video lamang"</string> + <string name="show_all" msgid="4780647751652596980">"Mga larawan at mga video"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Photo Gallery"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Walang Mga Larawan"</string> + <string name="crop_saved" msgid="4684933379430649946">"Na-save sa pag-download ang na-crop na larawan"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Hindi naka-save ang na-crop na larawan"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Walang mga gadget na magagamit"</string> + <string name="empty_album" msgid="6307897398825514762">"Walang available na mga larawan/video"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa Web Albums"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Gawing available sa offline"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Tapos na"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d ng %2$d na item:"</string> + <string name="title" msgid="7622928349908052569">"Pamagat"</string> + <string name="description" msgid="3016729318096557520">"Paglalarawan"</string> + <string name="time" msgid="1367953006052876956">"Oras"</string> + <string name="location" msgid="3432705876921618314">"Lokasyon"</string> + <string name="path" msgid="4725740395885105824">"Daanan"</string> + <string name="width" msgid="9215847239714321097">"Lapad"</string> + <string name="height" msgid="3648885449443787772">"Taas"</string> + <string name="orientation" msgid="4958327983165245513">"Pagsasaayos"</string> + <string name="duration" msgid="8160058911218541616">"Tagal"</string> + <string name="mimetype" msgid="3518268469266183548">"Uri ng MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Laki ng File"</string> + <string name="maker" msgid="7921835498034236197">"Tagagawa"</string> + <string name="model" msgid="8240207064064337366">"Modelo"</string> + <string name="flash" msgid="2816779031261147723">"Flash"</string> + <string name="aperture" msgid="5920657630303915195">"Aperture"</string> + <string name="focal_length" msgid="1291383769749877010">"Haba ng Focal"</string> + <string name="white_balance" msgid="8122534414851280901">"White Balance"</string> + <string name="exposure_time" msgid="3146642210127439553">"Exposure Time"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Manual"</string> + <string name="auto" msgid="4296941368722892821">"Auto"</string> + <string name="flash_on" msgid="7891556231891837284">"Flash fired"</string> + <string name="flash_off" msgid="1445443413822680010">"Walang flash"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Ginagawang available ang album offline"</item> + <item quantity="other" msgid="6929905722448632886">"Ginagawang available ang mga album offline"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Nakaimbak ang item na ito sa lokal at available sa offline."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Lahat ng Mga Album"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Mga Lokal na Album"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"Mga device na MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Picasa Albums"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> libre"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> o mababa"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> o mataas"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> sa <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"I-import"</string> + <string name="import_complete" msgid="1098450310074640619">"Kumpleto pag-import"</string> + <string name="import_fail" msgid="5205927625132482529">"Nabigo ang pag-import"</string> + <string name="camera_connected" msgid="6984353643349303075">"Nakakonekta ang camera"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Hindi nakakonekta ang camera"</string> + <string name="click_import" msgid="6407959065464291972">"Tumapik dito upang mag-import"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Mga larawan mula sa isa album"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"I-shuffle ang lahat ng larawan"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Pumili ng larawan"</string> + <string name="widget_type" msgid="7308564524449340985">"Uri ng Widget"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Slideshow"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Paunang kumukuha ng mga larawang picasa:"</string> + <string name="cache_status" msgid="7690438435538533106">"I-download ang <xliff:g id="NUMBER_0">%1$s</xliff:g> ng <xliff:g id="NUMBER_1">%2$s</xliff:g> (na) larawan"</string> + <string name="cache_done" msgid="9194449192869777483">"Kumpleto na ang pag-download"</string> + <string name="albums" msgid="7320787705180057947">"Mga Album"</string> + <string name="times" msgid="2023033894889499219">"Beses"</string> + <string name="locations" msgid="6649297994083130305">"Mga Lokasyon"</string> + <string name="people" msgid="4114003823747292747">"Mga Tao"</string> + <string name="tags" msgid="5539648765482935955">"Mga Tag"</string> + <string name="group_by" msgid="4308299657902209357">"Ipangkat ayon sa"</string> + <!-- no translation found for settings (1534847740615665736) --> + <skip /> + <string name="prefs_accounts" msgid="7942761992713671670">"Mga setting ng account"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Mga setting ng paggamit ng data"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Auto-upload"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Iba pang mga setting"</string> + <string name="about_gallery" msgid="8667445445883757255">"Tungkol sa Gallery"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Mag-sync lamang sa WiFi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Awtomatikong i-upload ang lahat ng larawan at video na iyong kinunan sa isang pribadong picasa album sa web"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Paganahin ang Auto-upload"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"NAKA-ON ang pag-sync ng mga larawan sa Google"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"NAKA-OFF ang pag-sync ng mga larawan sa Google"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Baguhin ang mga kagustuhan sa pag-sync o alisin ang account na ito"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Tingnan ang mga larawan at video mula sa account na ito sa Gallery"</string> + <string name="add_account" msgid="4271217504968243974">"Magdagdag ng account"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Pumili ng Auto-upload na account"</string> +</resources> diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml new file mode 100644 index 000000000..3f687228d --- /dev/null +++ b/res/values-tr/strings.xml @@ -0,0 +1,172 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Galeri"</string> + <string name="gadget_title" msgid="259405922673466798">"Resim çerçevesi"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Video oynatıcı"</string> + <string name="loading_video" msgid="4013492720121891585">"Video yükleniyor..."</string> + <string name="loading_image" msgid="1200894415793838191">"Resim yükleniyor…"</string> + <string name="loading_account" msgid="928195413034552034">"Hesap yükleniyor???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Videoyu sürdür"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Yürütme şuradan devam ettirilsin mi: %s ?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Yürütmeyi sürdür"</string> + <string name="loading" msgid="7038208555304563571">"Yükleniyor…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"Yüklenemedi"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Küçük resim yok"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Başlat"</string> + <string name="crop_save_text" msgid="8821167985419282305">"Tamam"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Başlamak için bir yüze hafifçe dokunun"</string> + <string name="saving_image" msgid="7270334453636349407">"Resim kaydediliyor..."</string> + <string name="crop_label" msgid="521114301871349328">"Resmi kırp"</string> + <string name="select_image" msgid="7841406150484742140">"Fotoğraf seçin"</string> + <string name="select_video" msgid="4859510992798615076">"Video seçin"</string> + <string name="select_item" msgid="2257529413100472599">"Öğeleri seçin"</string> + <string name="select_album" msgid="4632641262236697235">"Albümleri seçin"</string> + <string name="select_group" msgid="9090385962030340391">"Grupları seçin"</string> + <string name="set_image" msgid="2331476809308010401">"Resmi şu şekilde ayarla:"</string> + <string name="wallpaper" msgid="9222901738515471972">"Duvar kağıdı ayarlanıyor, lütfen bekleyin..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Duvar Kağıdı"</string> + <string name="delete" msgid="2839695998251824487">"Sil"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Silme İşlemini Onayla"</string> + <string name="cancel" msgid="3637516880917356226">"İptal"</string> + <string name="share" msgid="3619042788254195341">"Paylaş"</string> + <string name="select_all" msgid="8623593677101437957">"Tümünü Seç"</string> + <string name="deselect_all" msgid="7397531298370285581">"Tüm Seçimleri Kaldır"</string> + <string name="slideshow" msgid="4355906903247112975">"Slayt Gösterisi"</string> + <string name="details" msgid="8415120088556445230">"Ayrıntılar"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Kameraya Geç"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"Haritada göster"</string> + <string name="rotate_left" msgid="7412075232752726934">"Sola Döndür"</string> + <string name="rotate_right" msgid="7340681085011826618">"Sağa Döndür"</string> + <string name="no_such_item" msgid="3161074758669642065">"Öğe bulunamadı"</string> + <string name="edit" msgid="1502273844748580847">"Düzenle"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Kullanılabilir uygulama yok"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Önbelleğe Alma İsteklerini İşle"</string> + <string name="caching_label" msgid="3244800874547101776">"Önbelleğe alınıyor..."</string> + <string name="crop" msgid="7970750655414797277">"Kırp"</string> + <string name="set_as" msgid="3636764710790507868">"Şu şekilde ayarla:"</string> + <string name="video_err" msgid="7917736494827857757">"Video oynatılamıyor"</string> + <string name="group_by_location" msgid="316641628989023253">"Konuma göre"</string> + <string name="group_by_time" msgid="9046168567717963573">"Tarihe göre"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Etiketlere göre"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Kişilere göre"</string> + <string name="group_by_album" msgid="1532818636053818958">"Albüme göre"</string> + <string name="group_by_size" msgid="153766174950394155">"Boyuta göre"</string> + <string name="untagged" msgid="7281481064509590402">"Etiketlenmemiş"</string> + <string name="no_location" msgid="2036710947563713111">"Konum Bilgisi Yok"</string> + <string name="show_images_only" msgid="7263218480867672653">"Yalnızca resimler"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Yalnızca videolar"</string> + <string name="show_all" msgid="4780647751652596980">"Resimler ve videolar"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Fotoğraf Galerisi"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Fotoğraf Yok"</string> + <string name="crop_saved" msgid="4684933379430649946">"Kırpılmış resim indirilenler klasörüne kaydedildi"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Kırpılmış resim kaydedilmedi"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Kullanılabilir albüm yok"</string> + <string name="empty_album" msgid="6307897398825514762">"Kullanılabilir resim/video yok"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa Web Albümleri"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Çevrimdışı kullanılabilir yap"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Bitti"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d / %2$d öğe:"</string> + <string name="title" msgid="7622928349908052569">"Başlık"</string> + <string name="description" msgid="3016729318096557520">"Açıklama"</string> + <string name="time" msgid="1367953006052876956">"Saat"</string> + <string name="location" msgid="3432705876921618314">"Konum"</string> + <string name="path" msgid="4725740395885105824">"Yol"</string> + <string name="width" msgid="9215847239714321097">"Genişlik"</string> + <string name="height" msgid="3648885449443787772">"Yükseklik"</string> + <string name="orientation" msgid="4958327983165245513">"Yön"</string> + <string name="duration" msgid="8160058911218541616">"Süre"</string> + <string name="mimetype" msgid="3518268469266183548">"MIME Türü"</string> + <string name="file_size" msgid="4670384449129762138">"Dosya Boyutu"</string> + <string name="maker" msgid="7921835498034236197">"Yapımcı"</string> + <string name="model" msgid="8240207064064337366">"Model"</string> + <string name="flash" msgid="2816779031261147723">"Flaş"</string> + <string name="aperture" msgid="5920657630303915195">"Diyafram"</string> + <string name="focal_length" msgid="1291383769749877010">"Odak Uzaklığı"</string> + <string name="white_balance" msgid="8122534414851280901">"Beyaz Dengesi"</string> + <string name="exposure_time" msgid="3146642210127439553">"Pozlama Süresi"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"El ile"</string> + <string name="auto" msgid="4296941368722892821">"Otomatik"</string> + <string name="flash_on" msgid="7891556231891837284">"Flaş patladı"</string> + <string name="flash_off" msgid="1445443413822680010">"Flaş yok"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Albüm çevrimdışı kullanıma hazırlanıyor"</item> + <item quantity="other" msgid="6929905722448632886">"Albümler çevrimdışı kullanıma hazırlanıyor"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Bu öğe yerel olarak depolandı ve çevrimdışı kullanılabilir."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Tüm Albümler"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Yerel Albümler"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"MTP Cihazları"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Picasa Albümleri"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> boş"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> veya daha küçük"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> veya daha büyük"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> - <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"İçe aktar"</string> + <string name="import_complete" msgid="1098450310074640619">"İçe Aktrm Tamamlandı"</string> + <string name="import_fail" msgid="5205927625132482529">"İçe Aktarma Başarısız Oldu"</string> + <string name="camera_connected" msgid="6984353643349303075">"Kamera bağlandı"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Kamera bağlantısı kesildi"</string> + <string name="click_import" msgid="6407959065464291972">"İçe aktarmak için buraya dokunun"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Albümden resimler"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Tüm resimleri karıştır"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Resim seçin"</string> + <string name="widget_type" msgid="7308564524449340985">"Widget Türü"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Slayt gösterisi"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Picasa fotoğrafları önceden getiriliyor:"</string> + <string name="cache_status" msgid="7690438435538533106">"<xliff:g id="NUMBER_1">%2$s</xliff:g> fotoğraftan <xliff:g id="NUMBER_0">%1$s</xliff:g> tanesi indirildi"</string> + <string name="cache_done" msgid="9194449192869777483">"İndirme tamamlandı"</string> + <string name="albums" msgid="7320787705180057947">"Albümler"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"Konumlar"</string> + <string name="people" msgid="4114003823747292747">"Kişiler"</string> + <string name="tags" msgid="5539648765482935955">"Etiketler"</string> + <string name="group_by" msgid="4308299657902209357">"Grupla:"</string> + <string name="settings" msgid="1534847740615665736">"Ayarlar"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"Hesap ayarları"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Veri kullanım ayarları"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Otomatik yükleme"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Diğer ayarlar"</string> + <string name="about_gallery" msgid="8667445445883757255">"Galeri Hakkında"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Sadece Kablosuz\'da senkronize et"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Çektiğiniz tüm fotoğrafları ve videoları özel bir picasa web albümüne otomatik olarak yükleyin"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Otomatik yüklemeyi etkinleştir"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Google Foto senk AÇIK"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Google Foto senk KAPALI"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Senk tercihini değiştirin veya hesabı kaldırın"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Bu hesaptaki fotoğrafları ve videoları Galeri\'de görüntüle"</string> + <string name="add_account" msgid="4271217504968243974">"Hesap ekle"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Otomatk yükleme hesabını seçin"</string> +</resources> diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml new file mode 100644 index 000000000..c4ec3cffe --- /dev/null +++ b/res/values-uk/strings.xml @@ -0,0 +1,173 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Галерея"</string> + <string name="gadget_title" msgid="259405922673466798">"Фото-рамка"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Відеопрогравач"</string> + <string name="loading_video" msgid="4013492720121891585">"Завантаж. відео…"</string> + <string name="loading_image" msgid="1200894415793838191">"Завантаж. зображ…"</string> + <string name="loading_account" msgid="928195413034552034">"Завантаження облік. запису..."</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Відновити відео"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Продовж. відтворення з %s ?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Віднов. відтвор."</string> + <string name="loading" msgid="7038208555304563571">"Завантаж…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"Помилка завантаження"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Немає ескізу"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Почати знову"</string> + <string name="crop_save_text" msgid="8821167985419282305">"OK"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Натисн. лице, щоб поч."</string> + <string name="saving_image" msgid="7270334453636349407">"Зберіг-ня фото…"</string> + <string name="crop_label" msgid="521114301871349328">"Обрізати фото"</string> + <string name="select_image" msgid="7841406150484742140">"Виберіть фото"</string> + <string name="select_video" msgid="4859510992798615076">"Виберіть відео"</string> + <string name="select_item" msgid="2257529413100472599">"Виберіть елемент(-и)"</string> + <string name="select_album" msgid="4632641262236697235">"Виберіть альбом(-и)"</string> + <string name="select_group" msgid="9090385962030340391">"Виберіть групу(-и)"</string> + <string name="set_image" msgid="2331476809308010401">"Устан. фото як"</string> + <string name="wallpaper" msgid="9222901738515471972">"Встановл. фон. малюнка, зачекайте…"</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Фоновий мал."</string> + <string name="delete" msgid="2839695998251824487">"Видалити"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Підтверд. видал."</string> + <string name="cancel" msgid="3637516880917356226">"Скасувати"</string> + <string name="share" msgid="3619042788254195341">"Надісл."</string> + <string name="select_all" msgid="8623593677101437957">"Вибрати все"</string> + <string name="deselect_all" msgid="7397531298370285581">"Відмінити всі"</string> + <string name="slideshow" msgid="4355906903247112975">"Слайд-шоу"</string> + <string name="details" msgid="8415120088556445230">"Деталі"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Перейти до програми Камера"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"Показ. на карті"</string> + <string name="rotate_left" msgid="7412075232752726934">"Поверн. вліво"</string> + <string name="rotate_right" msgid="7340681085011826618">"Поверн. вправо"</string> + <string name="no_such_item" msgid="3161074758669642065">"Елемент не знайдено"</string> + <string name="edit" msgid="1502273844748580847">"Редагувати"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Немає доступних програм"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Обробка запитів кешування"</string> + <string name="caching_label" msgid="3244800874547101776">"Кешування..."</string> + <string name="crop" msgid="7970750655414797277">"Обріз."</string> + <string name="set_as" msgid="3636764710790507868">"Устан. як"</string> + <string name="video_err" msgid="7917736494827857757">"Неможл. відтвор. відео"</string> + <string name="group_by_location" msgid="316641628989023253">"За місцезнаходженням"</string> + <string name="group_by_time" msgid="9046168567717963573">"За часом"</string> + <string name="group_by_tags" msgid="3568731317210676160">"За тегами"</string> + <string name="group_by_faces" msgid="1566351636227274906">"За обличчями"</string> + <string name="group_by_album" msgid="1532818636053818958">"За альбомами"</string> + <string name="group_by_size" msgid="153766174950394155">"За розміром"</string> + <string name="untagged" msgid="7281481064509590402">"Без тегів"</string> + <string name="no_location" msgid="2036710947563713111">"Місцезн. не визнач."</string> + <string name="show_images_only" msgid="7263218480867672653">"Лише зображення"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Лише відео"</string> + <string name="show_all" msgid="4780647751652596980">"Зображення та відео"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Фотогалерея"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Фотографій немає"</string> + <string name="crop_saved" msgid="4684933379430649946">"Обрізане зображення збережено в папці завантажень"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Обрізане зображення не збережено"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Немає доступних альбомів"</string> + <string name="empty_album" msgid="6307897398825514762">"Немає доступних зображень або відео"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Веб-альбоми Picasa"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Зробити доступн. в реж. офлайн"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Готово"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d з %2$d елем.:"</string> + <string name="title" msgid="7622928349908052569">"Назва"</string> + <string name="description" msgid="3016729318096557520">"Опис"</string> + <string name="time" msgid="1367953006052876956">"Час"</string> + <string name="location" msgid="3432705876921618314">"Місце"</string> + <string name="path" msgid="4725740395885105824">"Шлях"</string> + <string name="width" msgid="9215847239714321097">"Ширина"</string> + <string name="height" msgid="3648885449443787772">"Висота"</string> + <string name="orientation" msgid="4958327983165245513">"Орієнтація"</string> + <string name="duration" msgid="8160058911218541616">"Тривалість"</string> + <string name="mimetype" msgid="3518268469266183548">"Тип MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Розмір файлу"</string> + <string name="maker" msgid="7921835498034236197">"Автор"</string> + <string name="model" msgid="8240207064064337366">"Модель"</string> + <string name="flash" msgid="2816779031261147723">"Flash"</string> + <string name="aperture" msgid="5920657630303915195">"Апертура"</string> + <string name="focal_length" msgid="1291383769749877010">"Фокусна відст."</string> + <string name="white_balance" msgid="8122534414851280901">"Баланс білого"</string> + <string name="exposure_time" msgid="3146642210127439553">"Час експозиції"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"мм"</string> + <string name="manual" msgid="6608905477477607865">"Вручну"</string> + <string name="auto" msgid="4296941368722892821">"Автомат."</string> + <string name="flash_on" msgid="7891556231891837284">"Викор. спалах"</string> + <string name="flash_off" msgid="1445443413822680010">"Без спалаху"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Надання доступу до альбому в режимі офлайн"</item> + <item quantity="other" msgid="6929905722448632886">"Надання доступу до альбомів у режимі офлайн"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Цей елемент зберігається локально та доступний у режимі офлайн."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Усі альбоми"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Локальні альбоми"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"Пристрої MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Альбоми Picasa"</string> + <string name="free_space_format" msgid="8766337315709161215">"Вільно <xliff:g id="BYTES">%s</xliff:g>"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> або менше"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> або більше"</string> + <string name="size_between" msgid="8779660840898917208">"від <xliff:g id="MIN_SIZE">%1$s</xliff:g> до <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Імпортувати"</string> + <string name="import_complete" msgid="1098450310074640619">"Імпорт завершено"</string> + <string name="import_fail" msgid="5205927625132482529">"Помилка імпорту"</string> + <string name="camera_connected" msgid="6984353643349303075">"Камеру підключено"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Камеру відключено"</string> + <string name="click_import" msgid="6407959065464291972">"Торкніться тут, щоб імпортувати"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Зображення з альбому"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Перемішати всі зображення"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Вибрати зображення"</string> + <string name="widget_type" msgid="7308564524449340985">"Тип віджета"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Слайд-шоу"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Попередня вибірка фотографій Picasa:"</string> + <string name="cache_status" msgid="7690438435538533106">"Завантажити <xliff:g id="NUMBER_0">%1$s</xliff:g> з <xliff:g id="NUMBER_1">%2$s</xliff:g> фото"</string> + <string name="cache_done" msgid="9194449192869777483">"Завантаження закінчено"</string> + <string name="albums" msgid="7320787705180057947">"Альбоми"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"Місцезнах."</string> + <string name="people" msgid="4114003823747292747">"Люди"</string> + <string name="tags" msgid="5539648765482935955">"Теги"</string> + <string name="group_by" msgid="4308299657902209357">"Групувати за"</string> + <!-- no translation found for settings (1534847740615665736) --> + <skip /> + <string name="prefs_accounts" msgid="7942761992713671670">"Налаштування облікового запису"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Налаштування використання даних"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Автоматичне завантаження"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Інші налаштування"</string> + <string name="about_gallery" msgid="8667445445883757255">"Про Галерею"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Синхронізація лише в мережі Wi-Fi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Автоматично завантажуйте всі зняті фото та відео в приватний веб-альбом Picasa"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Увімкнути автоматичне завантаження"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Синхр-цію фото Google УВІМКН."</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Синхр-цію фото Google ВИМКНЕНО"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Змін.налашт.синхр-ції чи видал.обл.запис"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Перегляд фото та відео з цього облікового запису в Галереї"</string> + <string name="add_account" msgid="4271217504968243974">"Додати обліковий запис"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Вибр.обл.зап.для авто-завант."</string> +</resources> diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml new file mode 100644 index 000000000..91836a04b --- /dev/null +++ b/res/values-vi/strings.xml @@ -0,0 +1,178 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Thư viện"</string> + <string name="gadget_title" msgid="259405922673466798">"Khung ảnh"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"Trình phát video"</string> + <string name="loading_video" msgid="4013492720121891585">"Đang tải video..."</string> + <string name="loading_image" msgid="1200894415793838191">"Đang tải ảnh…"</string> + <string name="loading_account" msgid="928195413034552034">"Đang tải tài khoản???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"Tiếp tục video"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Tiếp tục phát từ %s ?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Tiếp tục phát"</string> + <string name="loading" msgid="7038208555304563571">"Đang tải…"</string> + <string name="fail_to_load" msgid="2710120770735315683">"Không thể tải"</string> + <string name="no_thumbnail" msgid="284723185546429750">"Không có hình thu nhỏ"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"Bắt đầu lại"</string> + <string name="crop_save_text" msgid="8821167985419282305">"Ok"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"Nhấn vào một khuôn mặt để bắt đầu"</string> + <string name="saving_image" msgid="7270334453636349407">"Đang lưu ảnh…"</string> + <string name="crop_label" msgid="521114301871349328">"Cắt ảnh"</string> + <string name="select_image" msgid="7841406150484742140">"Chọn ảnh"</string> + <string name="select_video" msgid="4859510992798615076">"Chọn video"</string> + <string name="select_item" msgid="2257529413100472599">"Chọn (các) mục"</string> + <string name="select_album" msgid="4632641262236697235">"Chọn (các) anbom"</string> + <string name="select_group" msgid="9090385962030340391">"Chọn (các) nhóm"</string> + <string name="set_image" msgid="2331476809308010401">"Đặt ảnh làm"</string> + <string name="wallpaper" msgid="9222901738515471972">"Đang đặt hình nền, vui lòng đợi…"</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"Hình nền"</string> + <string name="delete" msgid="2839695998251824487">"Xoá"</string> + <string name="confirm_delete" msgid="5731757674837098707">"Xác nhận Xoá"</string> + <string name="cancel" msgid="3637516880917356226">"Hủy"</string> + <string name="share" msgid="3619042788254195341">"Chia sẻ"</string> + <string name="select_all" msgid="8623593677101437957">"Chọn Tất cả"</string> + <string name="deselect_all" msgid="7397531298370285581">"Bỏ chọn tất cả"</string> + <string name="slideshow" msgid="4355906903247112975">"Trình chiếu"</string> + <string name="details" msgid="8415120088556445230">"Chi tiết"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"Chuyển sang máy ảnh"</string> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"%1$d mục được chọn"</item> + <item quantity="one" msgid="2478365152745637768">"%1$d mục được chọn"</item> + <item quantity="other" msgid="754722656147810487">"%1$d mục được chọn"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"%1$d anbom được chọn"</item> + <item quantity="one" msgid="6184377003099987825">"%1$d anbom được chọn"</item> + <item quantity="other" msgid="53105607141906130">"%1$d anbom được chọn"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"%1$d nhóm được chọn"</item> + <item quantity="one" msgid="5030162638216034260">"%1$d nhóm được chọn"</item> + <item quantity="other" msgid="3512041363942842738">"%1$d nhóm được chọn"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"Hiển thị trên bản đồ"</string> + <string name="rotate_left" msgid="7412075232752726934">"Xoay Trái"</string> + <string name="rotate_right" msgid="7340681085011826618">"Xoay Phải"</string> + <string name="no_such_item" msgid="3161074758669642065">"Không tìm thấy mục nào"</string> + <string name="edit" msgid="1502273844748580847">"Chỉnh sửa"</string> + <string name="activity_not_found" msgid="3731390759313019518">"Không có ứng dụng nào sẵn có"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Xử lý yêu cầu bộ nhớ cache"</string> + <string name="caching_label" msgid="3244800874547101776">"Đang lưu vào bộ nhớ cache..."</string> + <string name="crop" msgid="7970750655414797277">"Cắt"</string> + <string name="set_as" msgid="3636764710790507868">"Đặt làm"</string> + <string name="video_err" msgid="7917736494827857757">"Không thể phát video"</string> + <string name="group_by_location" msgid="316641628989023253">"Theo vị trí"</string> + <string name="group_by_time" msgid="9046168567717963573">"Theo thời gian"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Theo thẻ"</string> + <string name="group_by_faces" msgid="1566351636227274906">"Theo người"</string> + <string name="group_by_album" msgid="1532818636053818958">"Theo anbom"</string> + <string name="group_by_size" msgid="153766174950394155">"Theo kích thước"</string> + <string name="untagged" msgid="7281481064509590402">"Không được gắn thẻ"</string> + <string name="no_location" msgid="2036710947563713111">"Không có vị trí nào"</string> + <string name="show_images_only" msgid="7263218480867672653">"Chỉ hình ảnh"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Chỉ video"</string> + <string name="show_all" msgid="4780647751652596980">"Hình ảnh và video"</string> + <string name="appwidget_title" msgid="6410561146863700411">"Thư viện ảnh"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"Không có ảnh nào"</string> + <string name="crop_saved" msgid="4684933379430649946">"Ảnh bị cắt đã được lưu vào thư mục tải xuống"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Ảnh bị cắt chưa được lưu"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Không có anbom nào"</string> + <string name="empty_album" msgid="6307897398825514762">"Không có hình ảnh/video nào sẵn có"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Anbom Web Picasa"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"Làm cho sẵn có khi ngoại tuyến"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Xong"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"%1$d/%2$d mục:"</string> + <string name="title" msgid="7622928349908052569">"Tiêu đề"</string> + <string name="description" msgid="3016729318096557520">"Mô tả"</string> + <string name="time" msgid="1367953006052876956">"Thời gian"</string> + <string name="location" msgid="3432705876921618314">"Vị trí"</string> + <string name="path" msgid="4725740395885105824">"Đường dẫn"</string> + <string name="width" msgid="9215847239714321097">"Chiều rộng"</string> + <string name="height" msgid="3648885449443787772">"Chiều cao"</string> + <string name="orientation" msgid="4958327983165245513">"Hướng"</string> + <string name="duration" msgid="8160058911218541616">"Thời lượng"</string> + <string name="mimetype" msgid="3518268469266183548">"Loại MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Kích thước tệp"</string> + <string name="maker" msgid="7921835498034236197">"Trình tạo"</string> + <string name="model" msgid="8240207064064337366">"Mẫu"</string> + <string name="flash" msgid="2816779031261147723">"Flash"</string> + <string name="aperture" msgid="5920657630303915195">"Khẩu độ"</string> + <string name="focal_length" msgid="1291383769749877010">"Tiêu cự"</string> + <string name="white_balance" msgid="8122534414851280901">"Cân bằng trắng"</string> + <string name="exposure_time" msgid="3146642210127439553">"T/g phơi sáng"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Thủ công"</string> + <string name="auto" msgid="4296941368722892821">"Tự động"</string> + <string name="flash_on" msgid="7891556231891837284">"Sử dụng flash"</string> + <string name="flash_off" msgid="1445443413822680010">"Không có flash"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"Tải xuống anbom để xem ở chế độ ngoại tuyến"</item> + <item quantity="other" msgid="6929905722448632886">"Tải xuống anbom để xem ở chế độ ngoại tuyến"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Mục này được lưu cục bộ và khả dụng ngoại tuyến."</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"Tất cả anbom"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"Anbom cục bộ"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"Thiết bị MTP"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Anbom Picasa"</string> + <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> trống"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> trở xuống"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> hoặc cao hơn"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> tới <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"Nhập"</string> + <string name="import_complete" msgid="1098450310074640619">"Nhập xong"</string> + <string name="import_fail" msgid="5205927625132482529">"Nhập không thành công"</string> + <string name="camera_connected" msgid="6984353643349303075">"Đã kết nối máy ảnh"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"Đã ngắt kết nối máy ảnh"</string> + <string name="click_import" msgid="6407959065464291972">"Chạm vào đây để nhập"</string> + <string name="widget_type_album" msgid="3245149644830731121">"Hình ảnh từ anbom"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"Hiển thị ngẫu nhiên tất cả hình ảnh"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"Chọn một hình ảnh"</string> + <string name="widget_type" msgid="7308564524449340985">"Loại tiện ích con"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"Trình chiếu"</string> + <string name="cache_status_title" msgid="8414708919928621485">"Tìm nạp trước ảnh picasa:"</string> + <string name="cache_status" msgid="7690438435538533106">"Tải xuống <xliff:g id="NUMBER_0">%1$s</xliff:g> trong tổng số <xliff:g id="NUMBER_1">%2$s</xliff:g> ảnh"</string> + <string name="cache_done" msgid="9194449192869777483">"Tải xuống hoàn tất"</string> + <string name="albums" msgid="7320787705180057947">"Anbom"</string> + <string name="times" msgid="2023033894889499219">"Lần"</string> + <string name="locations" msgid="6649297994083130305">"Địa điểm"</string> + <string name="people" msgid="4114003823747292747">"Mọi người"</string> + <string name="tags" msgid="5539648765482935955">"Thẻ"</string> + <string name="group_by" msgid="4308299657902209357">"Nhóm theo"</string> + <!-- no translation found for settings (1534847740615665736) --> + <skip /> + <string name="prefs_accounts" msgid="7942761992713671670">"Cài đặt tài khoản"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"Cài đặt sử dụng dữ liệu"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"Tự động tải lên"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"Cài đặt khác"</string> + <string name="about_gallery" msgid="8667445445883757255">"Giới thiệu về thư viện"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"Chỉ đồng bộ hóa trên WiFi"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"Tự động tải tất cả ảnh và video bạn chụp hoặc quay lên anbom web picasa riêng tư"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"Bật tự động tải lên"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Đồng bộ hóa ảnh trên Google đã BẬT"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Đồng bộ hóa ảnh trên Google đã Tắt"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"Thay đổi tùy chọn đồng bộ hóa hoặc xóa tài khoản này"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"Xem ảnh và video từ tài khoản này trong Thư viện"</string> + <string name="add_account" msgid="4271217504968243974">"Thêm tài khoản"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"Chọn tài khoản tự động tải lên"</string> +</resources> diff --git a/res/values-w1024dp/strings.xml b/res/values-w1024dp/strings.xml new file mode 100644 index 000000000..39903f43c --- /dev/null +++ b/res/values-w1024dp/strings.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2007 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. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- String indicating how many media item(s) is(are) selected + eg. 1 item selected [CHAR LIMIT=30] --> + <plurals name="number_of_items_selected"> + <item quantity="zero">%1$d item selected</item> + <item quantity="one">%1$d item selected</item> + <item quantity="other">%1$d items selected</item> + </plurals> + + <!-- String indicating how many media album(s) is(are) selected + eg. 1 album selected [CHAR LIMIT=30] --> + <plurals name="number_of_albums_selected"> + <item quantity="zero">%1$d album selected</item> + <item quantity="one">%1$d album selected</item> + <item quantity="other">%1$d albums selected</item> + </plurals> + + <!-- String indicating how many media group(s) is(are) selected + eg. 1 group selected [CHAR LIMIT=30] --> + <plurals name="number_of_groups_selected"> + <item quantity="zero">%1$d group selected</item> + <item quantity="one">%1$d group selected</item> + <item quantity="other">%1$d groups selected</item> + </plurals> + +</resources>
\ No newline at end of file diff --git a/res/values-xlarge/dimensions.xml b/res/values-xlarge/dimensions.xml new file mode 100644 index 000000000..4ead09c71 --- /dev/null +++ b/res/values-xlarge/dimensions.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<resources> + <dimen name="appwidget_width">240dp</dimen> + <dimen name="appwidget_height">200dp</dimen> + <dimen name="stack_photo_width">230dp</dimen> + <dimen name="stack_photo_height">190dp</dimen> + + <!-- configuration for album set page --> + <dimen name="albumset_display_item_size">144dp</dimen> + <dimen name="albumset_slot_width">220dp</dimen> + <dimen name="albumset_slot_height">220dp</dimen> + <dimen name="albumset_label_font_size">14dp</dimen> + <dimen name="albumset_label_offset_y">110dp</dimen> + + <!-- configuration for album page --> + <dimen name="album_display_item_size">176dp</dimen> + <dimen name="album_slot_width">192dp</dimen> + <dimen name="album_slot_height">192dp</dimen> + + <!-- configuration for manage page --> + <dimen name="cache_bar_height">48dp</dimen> + <dimen name="cache_bar_pin_left_margin">16dp</dimen> + <dimen name="cache_bar_pin_right_margin">8dp</dimen> + <dimen name="cache_bar_button_right_margin">8dp</dimen> + <dimen name="cache_bar_font_size">18dp</dimen> +</resources> diff --git a/res/values-xlarge/styles.xml b/res/values-xlarge/styles.xml new file mode 100644 index 000000000..494fd9cc6 --- /dev/null +++ b/res/values-xlarge/styles.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <style name="Theme.Gallery" parent="android:Theme.Holo"> + <item name="android:displayOptions"></item> + <item name="android:actionBarStyle">@style/Holo.ActionBar</item> + </style> +</resources> diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml new file mode 100644 index 000000000..f7d8fe745 --- /dev/null +++ b/res/values-zh-rCN/strings.xml @@ -0,0 +1,177 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"图库"</string> + <string name="gadget_title" msgid="259405922673466798">"相框"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"视频播放器"</string> + <string name="loading_video" msgid="4013492720121891585">"正在载入视频..."</string> + <string name="loading_image" msgid="1200894415793838191">"正在载入图片..."</string> + <string name="loading_account" msgid="928195413034552034">"正在加载帐户???"</string> + <string name="resume_playing_title" msgid="8996677350649355013">"继续播放视频"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"从 %s 开始继续播放?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"继续播放"</string> + <string name="loading" msgid="7038208555304563571">"正在载入..."</string> + <string name="fail_to_load" msgid="2710120770735315683">"载入失败"</string> + <string name="no_thumbnail" msgid="284723185546429750">"无缩略图"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"重新开始"</string> + <string name="crop_save_text" msgid="8821167985419282305">"确定"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"轻点一张脸开始裁剪。"</string> + <string name="saving_image" msgid="7270334453636349407">"正在保存照片..."</string> + <string name="crop_label" msgid="521114301871349328">"修剪照片"</string> + <string name="select_image" msgid="7841406150484742140">"选择照片"</string> + <string name="select_video" msgid="4859510992798615076">"选择视频"</string> + <string name="select_item" msgid="2257529413100472599">"选择项目"</string> + <string name="select_album" msgid="4632641262236697235">"选择相册"</string> + <string name="select_group" msgid="9090385962030340391">"选择群组"</string> + <string name="set_image" msgid="2331476809308010401">"将照片设置为"</string> + <string name="wallpaper" msgid="9222901738515471972">"正在设置壁纸,请稍候..."</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"壁纸"</string> + <string name="delete" msgid="2839695998251824487">"删除"</string> + <string name="confirm_delete" msgid="5731757674837098707">"确认删除"</string> + <string name="cancel" msgid="3637516880917356226">"取消"</string> + <string name="share" msgid="3619042788254195341">"分享"</string> + <string name="select_all" msgid="8623593677101437957">"全选"</string> + <string name="deselect_all" msgid="7397531298370285581">"取消全选"</string> + <string name="slideshow" msgid="4355906903247112975">"播放幻灯片"</string> + <string name="details" msgid="8415120088556445230">"详细信息"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"切换到相机"</string> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"已选中 %1$d 项"</item> + <item quantity="one" msgid="2478365152745637768">"选中了 %1$d 项"</item> + <item quantity="other" msgid="754722656147810487">"选中了 %1$d 项"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"选中了 %1$d 个相册"</item> + <item quantity="one" msgid="6184377003099987825">"选中了 %1$d 个相册"</item> + <item quantity="other" msgid="53105607141906130">"选中了 %1$d 个相册"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"选中了 %1$d 组"</item> + <item quantity="one" msgid="5030162638216034260">"选中了 %1$d 组"</item> + <item quantity="other" msgid="3512041363942842738">"选中了 %1$d 组"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"显示在地图上"</string> + <string name="rotate_left" msgid="7412075232752726934">"向左旋转"</string> + <string name="rotate_right" msgid="7340681085011826618">"向右旋转"</string> + <string name="no_such_item" msgid="3161074758669642065">"未找到相应条目"</string> + <string name="edit" msgid="1502273844748580847">"编辑"</string> + <string name="activity_not_found" msgid="3731390759313019518">"没有可用的应用程序"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"处理缓存请求"</string> + <string name="caching_label" msgid="3244800874547101776">"正在缓存..."</string> + <string name="crop" msgid="7970750655414797277">"修剪"</string> + <string name="set_as" msgid="3636764710790507868">"设置为"</string> + <string name="video_err" msgid="7917736494827857757">"无法播放视频"</string> + <string name="group_by_location" msgid="316641628989023253">"按位置分组"</string> + <string name="group_by_time" msgid="9046168567717963573">"按时间分组"</string> + <string name="group_by_tags" msgid="3568731317210676160">"按标签分组"</string> + <string name="group_by_faces" msgid="1566351636227274906">"按人物"</string> + <string name="group_by_album" msgid="1532818636053818958">"按相册分组"</string> + <string name="group_by_size" msgid="153766174950394155">"按大小分组"</string> + <string name="untagged" msgid="7281481064509590402">"未加标签"</string> + <string name="no_location" msgid="2036710947563713111">"无地点"</string> + <string name="show_images_only" msgid="7263218480867672653">"仅限图片"</string> + <string name="show_videos_only" msgid="3850394623678871697">"仅限视频"</string> + <string name="show_all" msgid="4780647751652596980">"图片和视频"</string> + <string name="appwidget_title" msgid="6410561146863700411">"照片库"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"无照片"</string> + <string name="crop_saved" msgid="4684933379430649946">"经过裁剪的照片已保存至下载文件夹"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"经过裁剪的照片尚未保存"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"没有可用的相册"</string> + <string name="empty_album" msgid="6307897398825514762">"没有可用的图片/视频"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa 网络相册"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"允许离线状态下使用"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"完成"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"第 %1$d 个项(共 %2$d 个):"</string> + <string name="title" msgid="7622928349908052569">"标题"</string> + <string name="description" msgid="3016729318096557520">"描述"</string> + <string name="time" msgid="1367953006052876956">"时间"</string> + <string name="location" msgid="3432705876921618314">"地点"</string> + <string name="path" msgid="4725740395885105824">"路径"</string> + <string name="width" msgid="9215847239714321097">"宽度"</string> + <string name="height" msgid="3648885449443787772">"高度"</string> + <string name="orientation" msgid="4958327983165245513">"浏览模式"</string> + <string name="duration" msgid="8160058911218541616">"时长"</string> + <string name="mimetype" msgid="3518268469266183548">"MIME 类型"</string> + <string name="file_size" msgid="4670384449129762138">"文件大小"</string> + <string name="maker" msgid="7921835498034236197">"制造商"</string> + <string name="model" msgid="8240207064064337366">"模型"</string> + <string name="flash" msgid="2816779031261147723">"闪光灯"</string> + <string name="aperture" msgid="5920657630303915195">"光圈"</string> + <string name="focal_length" msgid="1291383769749877010">"焦距"</string> + <string name="white_balance" msgid="8122534414851280901">"白平衡"</string> + <string name="exposure_time" msgid="3146642210127439553">"曝光时间"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"手动"</string> + <string name="auto" msgid="4296941368722892821">"自动"</string> + <string name="flash_on" msgid="7891556231891837284">"使用了闪光灯"</string> + <string name="flash_off" msgid="1445443413822680010">"未使用闪光灯"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"允许离线查看相册"</item> + <item quantity="other" msgid="6929905722448632886">"允许离线查看相册"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"该项是存储在本地的,可在离线状态下使用。"</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"所有相册"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"本地相册"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"MTP 设备"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Picasa 相册"</string> + <string name="free_space_format" msgid="8766337315709161215">"可用空间:<xliff:g id="BYTES">%s</xliff:g>"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> 或更小"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> 或更大"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> 到 <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"导入"</string> + <string name="import_complete" msgid="1098450310074640619">"导入已完成"</string> + <string name="import_fail" msgid="5205927625132482529">"导入失败"</string> + <string name="camera_connected" msgid="6984353643349303075">"相机已连接"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"相机已断开连接"</string> + <string name="click_import" msgid="6407959065464291972">"触摸此处可导入"</string> + <string name="widget_type_album" msgid="3245149644830731121">"相册中的图片"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"随机显示所有图片"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"选择一张图片"</string> + <string name="widget_type" msgid="7308564524449340985">"窗口小部件类型"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"幻灯片"</string> + <string name="cache_status_title" msgid="8414708919928621485">"预先抓取 Picasa 照片:"</string> + <string name="cache_status" msgid="7690438435538533106">"已下载 <xliff:g id="NUMBER_0">%1$s</xliff:g> 张照片,共 <xliff:g id="NUMBER_1">%2$s</xliff:g> 张"</string> + <string name="cache_done" msgid="9194449192869777483">"下载完成"</string> + <string name="albums" msgid="7320787705180057947">"相册"</string> + <string name="times" msgid="2023033894889499219">"时间"</string> + <string name="locations" msgid="6649297994083130305">"地点"</string> + <string name="people" msgid="4114003823747292747">"人物"</string> + <string name="tags" msgid="5539648765482935955">"标签"</string> + <string name="group_by" msgid="4308299657902209357">"分组依据"</string> + <string name="settings" msgid="1534847740615665736">"设置"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"帐户设置"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"数据使用设置"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"自动上传"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"其他设置"</string> + <string name="about_gallery" msgid="8667445445883757255">"关于图库"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"仅通过 WiFi 同步"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"自动将您拍摄的所有照片和视频上传到私人 Picasa 网络相册"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"启用自动上传"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Google 相册同步功能已开启"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Google 相册同步功能已关闭"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"更改同步偏好设置或删除此帐户"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"在图库中查看此帐户的照片和视频"</string> + <string name="add_account" msgid="4271217504968243974">"添加帐户"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"选择用于自动上传的帐户"</string> +</resources> diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml new file mode 100644 index 000000000..a2cace6bb --- /dev/null +++ b/res/values-zh-rTW/strings.xml @@ -0,0 +1,172 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"圖片庫"</string> + <string name="gadget_title" msgid="259405922673466798">"相框"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <string name="movie_view_label" msgid="3526526872644898229">"影片播放器"</string> + <string name="loading_video" msgid="4013492720121891585">"正在載入影片…"</string> + <string name="loading_image" msgid="1200894415793838191">"正在載入圖片..."</string> + <string name="loading_account" msgid="928195413034552034">"正在載入帳戶..."</string> + <string name="resume_playing_title" msgid="8996677350649355013">"繼續播放影片"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"要從 %s 繼續播放嗎?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"繼續播放"</string> + <string name="loading" msgid="7038208555304563571">"載入中..."</string> + <string name="fail_to_load" msgid="2710120770735315683">"無法載入"</string> + <string name="no_thumbnail" msgid="284723185546429750">"無縮圖"</string> + <string name="resume_playing_restart" msgid="5471008499835769292">"重新開始"</string> + <string name="crop_save_text" msgid="8821167985419282305">"確定"</string> + <string name="multiface_crop_help" msgid="3127018992717032779">"輕觸所需的臉孔開始裁剪。"</string> + <string name="saving_image" msgid="7270334453636349407">"正在儲存相片..."</string> + <string name="crop_label" msgid="521114301871349328">"裁剪相片"</string> + <string name="select_image" msgid="7841406150484742140">"選取相片"</string> + <string name="select_video" msgid="4859510992798615076">"選取影片"</string> + <string name="select_item" msgid="2257529413100472599">"選取項目"</string> + <string name="select_album" msgid="4632641262236697235">"選取相簿"</string> + <string name="select_group" msgid="9090385962030340391">"選取群組"</string> + <string name="set_image" msgid="2331476809308010401">"將相片設為"</string> + <string name="wallpaper" msgid="9222901738515471972">"設定桌布中,請稍候…"</string> + <string name="camera_setas_wallpaper" msgid="797463183863414289">"桌布"</string> + <string name="delete" msgid="2839695998251824487">"刪除"</string> + <string name="confirm_delete" msgid="5731757674837098707">"確認刪除"</string> + <string name="cancel" msgid="3637516880917356226">"取消"</string> + <string name="share" msgid="3619042788254195341">"分享"</string> + <string name="select_all" msgid="8623593677101437957">"全選"</string> + <string name="deselect_all" msgid="7397531298370285581">"取消選取全部"</string> + <string name="slideshow" msgid="4355906903247112975">"投影播放"</string> + <string name="details" msgid="8415120088556445230">"詳細資料"</string> + <string name="switch_to_camera" msgid="7280111806675169992">"切換至相機"</string> + <!-- no translation found for number_of_items_selected:zero (2142579311530586258) --> + <!-- no translation found for number_of_items_selected:one (2478365152745637768) --> + <!-- no translation found for number_of_items_selected:other (754722656147810487) --> + <!-- no translation found for number_of_albums_selected:zero (749292746814788132) --> + <!-- no translation found for number_of_albums_selected:one (6184377003099987825) --> + <!-- no translation found for number_of_albums_selected:other (53105607141906130) --> + <!-- no translation found for number_of_groups_selected:zero (3466388370310869238) --> + <!-- no translation found for number_of_groups_selected:one (5030162638216034260) --> + <!-- no translation found for number_of_groups_selected:other (3512041363942842738) --> + <string name="show_on_map" msgid="6157544221201750980">"在地圖上顯示"</string> + <string name="rotate_left" msgid="7412075232752726934">"向左旋轉"</string> + <string name="rotate_right" msgid="7340681085011826618">"向右旋轉"</string> + <string name="no_such_item" msgid="3161074758669642065">"找不到項目"</string> + <string name="edit" msgid="1502273844748580847">"編輯"</string> + <string name="activity_not_found" msgid="3731390759313019518">"沒有可用的應用程式"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"處理快取要求"</string> + <string name="caching_label" msgid="3244800874547101776">"快取中..."</string> + <string name="crop" msgid="7970750655414797277">"裁剪"</string> + <string name="set_as" msgid="3636764710790507868">"設為"</string> + <string name="video_err" msgid="7917736494827857757">"無法播放影片"</string> + <string name="group_by_location" msgid="316641628989023253">"依位置"</string> + <string name="group_by_time" msgid="9046168567717963573">"依時間"</string> + <string name="group_by_tags" msgid="3568731317210676160">"依標記"</string> + <string name="group_by_faces" msgid="1566351636227274906">"依人物分組"</string> + <string name="group_by_album" msgid="1532818636053818958">"依專輯"</string> + <string name="group_by_size" msgid="153766174950394155">"依大小分類"</string> + <string name="untagged" msgid="7281481064509590402">"無標記"</string> + <string name="no_location" msgid="2036710947563713111">"無位置資訊"</string> + <string name="show_images_only" msgid="7263218480867672653">"僅顯示圖片"</string> + <string name="show_videos_only" msgid="3850394623678871697">"僅顯示影片"</string> + <string name="show_all" msgid="4780647751652596980">"圖片和影片"</string> + <string name="appwidget_title" msgid="6410561146863700411">"相片庫"</string> + <string name="appwidget_empty_text" msgid="4123016777080388680">"沒有任何相片"</string> + <string name="crop_saved" msgid="4684933379430649946">"裁剪圖片已儲存至下載資料夾"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"未儲存裁剪圖片"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"沒有可用的相簿"</string> + <string name="empty_album" msgid="6307897398825514762">"沒有可用的圖片/影片"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Picasa 網路相簿"</string> + <string name="picasa_posts" msgid="1055151689217481993">"Buzz"</string> + <string name="make_available_offline" msgid="5157950985488297112">"可在離線時使用"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"完成"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"第 %1$d 個項目,共 %2$d 個項目:"</string> + <string name="title" msgid="7622928349908052569">"標題"</string> + <string name="description" msgid="3016729318096557520">"說明"</string> + <string name="time" msgid="1367953006052876956">"時間"</string> + <string name="location" msgid="3432705876921618314">"地點"</string> + <string name="path" msgid="4725740395885105824">"路徑"</string> + <string name="width" msgid="9215847239714321097">"寬度"</string> + <string name="height" msgid="3648885449443787772">"高度"</string> + <string name="orientation" msgid="4958327983165245513">"瀏覽模式"</string> + <string name="duration" msgid="8160058911218541616">"影片長度"</string> + <string name="mimetype" msgid="3518268469266183548">"MIME 類型"</string> + <string name="file_size" msgid="4670384449129762138">"檔案大小"</string> + <string name="maker" msgid="7921835498034236197">"製造商"</string> + <string name="model" msgid="8240207064064337366">"型號"</string> + <string name="flash" msgid="2816779031261147723">"閃光燈"</string> + <string name="aperture" msgid="5920657630303915195">"光圈"</string> + <string name="focal_length" msgid="1291383769749877010">"焦距"</string> + <string name="white_balance" msgid="8122534414851280901">"白平衡"</string> + <string name="exposure_time" msgid="3146642210127439553">"曝光時間"</string> + <string name="iso" msgid="5028296664327335940">"ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"釐米"</string> + <string name="manual" msgid="6608905477477607865">"手動"</string> + <string name="auto" msgid="4296941368722892821">"自動"</string> + <string name="flash_on" msgid="7891556231891837284">"使用閃光燈"</string> + <string name="flash_off" msgid="1445443413822680010">"未使用閃光燈"</string> + <plurals name="make_albums_available_offline"> + <item quantity="one" msgid="2955975726887896888">"正在將相簿設為可離線瀏覽"</item> + <item quantity="other" msgid="6929905722448632886">"正在將相簿設為可離線瀏覽"</item> + </plurals> + <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"這個項目已儲存在本機上,並且可供離線使用。"</string> + <string name="set_label_all_albums" msgid="3507256844918130594">"所有相簿"</string> + <string name="set_label_local_albums" msgid="5227548825039781">"本機相簿"</string> + <string name="set_label_mtp_devices" msgid="5779788799122828528">"MTP 裝置"</string> + <string name="set_label_picasa_albums" msgid="2736308697306982589">"Picasa 相簿"</string> + <string name="free_space_format" msgid="8766337315709161215">"可用空間:<xliff:g id="BYTES">%s</xliff:g>"</string> + <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> 以下"</string> + <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> 以上"</string> + <string name="size_between" msgid="8779660840898917208">"<xliff:g id="MIN_SIZE">%1$s</xliff:g> 至 <xliff:g id="MAX_SIZE">%2$s</xliff:g>"</string> + <string name="Import" msgid="3985447518557474672">"匯入"</string> + <string name="import_complete" msgid="1098450310074640619">"匯入完成"</string> + <string name="import_fail" msgid="5205927625132482529">"匯入失敗"</string> + <string name="camera_connected" msgid="6984353643349303075">"相機已連線"</string> + <string name="camera_disconnected" msgid="3683036560562699311">"相機已中斷連線"</string> + <string name="click_import" msgid="6407959065464291972">"輕觸這裡即可匯入相簿"</string> + <string name="widget_type_album" msgid="3245149644830731121">"相簿中的圖片"</string> + <string name="widget_type_shuffle" msgid="8594622705019763768">"隨機播放所有圖片"</string> + <string name="widget_type_photo" msgid="8384174698965738770">"選擇圖片"</string> + <string name="widget_type" msgid="7308564524449340985">"小工具類型"</string> + <string name="slideshow_dream_name" msgid="6915963319933437083">"投影播放"</string> + <string name="cache_status_title" msgid="8414708919928621485">"正在預先擷取 Picasa 相片:"</string> + <string name="cache_status" msgid="7690438435538533106">"下載 <xliff:g id="NUMBER_0">%1$s</xliff:g> 張相片 (共 <xliff:g id="NUMBER_1">%2$s</xliff:g> 張)"</string> + <string name="cache_done" msgid="9194449192869777483">"下載完成"</string> + <string name="albums" msgid="7320787705180057947">"相簿"</string> + <!-- no translation found for times (2023033894889499219) --> + <skip /> + <string name="locations" msgid="6649297994083130305">"位置"</string> + <string name="people" msgid="4114003823747292747">"人物"</string> + <string name="tags" msgid="5539648765482935955">"標記"</string> + <string name="group_by" msgid="4308299657902209357">"分組依據"</string> + <string name="settings" msgid="1534847740615665736">"設定"</string> + <string name="prefs_accounts" msgid="7942761992713671670">"帳戶設定"</string> + <string name="prefs_data_usage" msgid="410592732727343215">"資料用量設定"</string> + <string name="prefs_auto_upload" msgid="2467627128066665126">"自動上傳"</string> + <string name="prefs_other_settings" msgid="6034181851440646681">"其他設定"</string> + <string name="about_gallery" msgid="8667445445883757255">"關於圖片庫"</string> + <string name="sync_on_wifi_only" msgid="5795753226259399958">"僅透過 Wi-Fi 進行同步處理"</string> + <string name="helptext_auto_upload" msgid="133741242503097377">"自動將您拍攝的所有相片和影片上傳至私人 Picasa 網路相簿"</string> + <string name="enable_auto_upload" msgid="1586329406342131">"啟用自動上傳"</string> + <string name="photo_sync_is_on" msgid="1653898269297050634">"Google 相片同步功能已開啟"</string> + <string name="photo_sync_is_off" msgid="6464193461664544289">"Google 相片同步功能已關閉"</string> + <string name="helptext_photo_sync" msgid="8617245939103545623">"變更同步偏好設定或移除這個帳戶"</string> + <string name="view_photo_for_account" msgid="5608040380422337939">"在「圖片庫」中查看這個帳戶的相片和影片"</string> + <string name="add_account" msgid="4271217504968243974">"新增帳戶"</string> + <string name="auto_upload_chooser_title" msgid="1494524693870792948">"選擇自動上傳帳戶"</string> +</resources> diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml new file mode 100644 index 000000000..9a0bf814d --- /dev/null +++ b/res/values-zu/strings.xml @@ -0,0 +1,233 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (C) 2007 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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name" msgid="1928079047368929634">"Igalari"</string> + <string name="gadget_title" msgid="259405922673466798">"Uhlaka lwesithombe"</string> + <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string> + <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string> + <!-- outdated translation 3697303290960009886 --> <string name="movie_view_label" msgid="3526526872644898229">"Ama-movie"</string> + <string name="loading_video" msgid="4013492720121891585">"Ilayisha ividiyo..."</string> + <string name="loading_image" msgid="1200894415793838191">"Iyalayisha isithombe..."</string> + <!-- no translation found for loading_account (928195413034552034) --> + <skip /> + <string name="resume_playing_title" msgid="8996677350649355013">"Qalisa ividiyo"</string> + <string name="resume_playing_message" msgid="5184414518126703481">"Qalisa ukudlala kusuka %s?"</string> + <string name="resume_playing_resume" msgid="3847915469173852416">"Qalisa ukudlala"</string> + <string name="loading" msgid="7038208555304563571">"Iyalayisha..."</string> + <!-- outdated translation 3355969119388837437 --> <string name="fail_to_load" msgid="2710120770735315683">"Yehlulekile ukulayisha"</string> + <!-- no translation found for no_thumbnail (284723185546429750) --> + <skip /> + <string name="resume_playing_restart" msgid="5471008499835769292">"Qala phansi"</string> + <string name="crop_save_text" msgid="8821167985419282305">"Kulungile"</string> + <!-- no translation found for multiface_crop_help (3127018992717032779) --> + <skip /> + <string name="saving_image" msgid="7270334453636349407">"Ilondoloza isithombe..."</string> + <!-- no translation found for crop_label (521114301871349328) --> + <skip /> + <string name="select_image" msgid="7841406150484742140">"Khetha isithombe"</string> + <string name="select_video" msgid="4859510992798615076">"Khetha ividiyo"</string> + <string name="select_item" msgid="2257529413100472599">"Khetha intwana(izi)"</string> + <string name="select_album" msgid="4632641262236697235">"Khetha i-albhamu(ama)"</string> + <string name="select_group" msgid="9090385962030340391">"Khetha iqembu(ama)"</string> + <string name="set_image" msgid="2331476809308010401">"Hlela isithombe njenge"</string> + <!-- no translation found for wallpaper (9222901738515471972) --> + <skip /> + <!-- no translation found for camera_setas_wallpaper (797463183863414289) --> + <skip /> + <!-- no translation found for delete (2839695998251824487) --> + <skip /> + <string name="confirm_delete" msgid="5731757674837098707">"Qinisekisa Ukususa"</string> + <!-- no translation found for cancel (3637516880917356226) --> + <skip /> + <string name="share" msgid="3619042788254195341">"Yabelana"</string> + <string name="select_all" msgid="8623593677101437957">"Khetha konke"</string> + <string name="deselect_all" msgid="7397531298370285581">"Ungakhethi Konke"</string> + <string name="slideshow" msgid="4355906903247112975">"Umbukiso weslaydi"</string> + <!-- no translation found for details (8415120088556445230) --> + <skip /> + <!-- no translation found for switch_to_camera (7280111806675169992) --> + <skip /> + <plurals name="number_of_items_selected"> + <item quantity="zero" msgid="2142579311530586258">"%1$d khethiwe"</item> + <item quantity="one" msgid="2478365152745637768">"%1$d khethiwe"</item> + <item quantity="other" msgid="754722656147810487">"%1$d khethiwe"</item> + </plurals> + <plurals name="number_of_albums_selected"> + <item quantity="zero" msgid="749292746814788132">"%1$d khethiwe"</item> + <item quantity="one" msgid="6184377003099987825">"%1$d khethiwe"</item> + <item quantity="other" msgid="53105607141906130">"%1$d khethiwe"</item> + </plurals> + <plurals name="number_of_groups_selected"> + <item quantity="zero" msgid="3466388370310869238">"%1$d khethiwe"</item> + <item quantity="one" msgid="5030162638216034260">"%1$d khethiwe"</item> + <item quantity="other" msgid="3512041363942842738">"%1$d khethiwe"</item> + </plurals> + <string name="show_on_map" msgid="6157544221201750980">"Bonisa kwimephu"</string> + <string name="rotate_left" msgid="7412075232752726934">"Phendula ngakwesobunxele"</string> + <string name="rotate_right" msgid="7340681085011826618">"Phendula ngakwesokudla"</string> + <string name="no_such_item" msgid="3161074758669642065">"Intwana ayitholwanga"</string> + <!-- no translation found for edit (1502273844748580847) --> + <skip /> + <string name="activity_not_found" msgid="3731390759313019518">"Alukho uhlelo lokusebenza olutholakalayo"</string> + <string name="process_caching_requests" msgid="1076938190997999614">"Izicelo Zokulondoloza Okwesikhashana Inqubo"</string> + <string name="caching_label" msgid="3244800874547101776">"Ukulondoloza isikhashana..."</string> + <string name="crop" msgid="7970750655414797277">"Nqampuna"</string> + <string name="set_as" msgid="3636764710790507868">"Hlela njenge"</string> + <string name="video_err" msgid="7917736494827857757">"Ayikwazi ukudlala ividiyo"</string> + <string name="group_by_location" msgid="316641628989023253">"Ngendawo"</string> + <string name="group_by_time" msgid="9046168567717963573">"Ngesikhathi"</string> + <string name="group_by_tags" msgid="3568731317210676160">"Ngamamaki"</string> + <!-- no translation found for group_by_faces (1566351636227274906) --> + <skip /> + <string name="group_by_album" msgid="1532818636053818958">"Nge-albhamu"</string> + <!-- no translation found for group_by_size (153766174950394155) --> + <skip /> + <string name="untagged" msgid="7281481064509590402">"Akunasilengiso"</string> + <string name="no_location" msgid="2036710947563713111">"Ayikho Indawo"</string> + <string name="show_images_only" msgid="7263218480867672653">"Izithombe kuphela"</string> + <string name="show_videos_only" msgid="3850394623678871697">"Amavidiyo kuphela"</string> + <string name="show_all" msgid="4780647751652596980">"Izithombe namavidiyo"</string> + <!-- no translation found for appwidget_title (6410561146863700411) --> + <skip /> + <!-- no translation found for appwidget_empty_text (4123016777080388680) --> + <skip /> + <string name="crop_saved" msgid="4684933379430649946">"Isithombe esinqampuliwe silondolozwe ekulandeni"</string> + <string name="crop_not_saved" msgid="1438309290700431923">"Umfanekiso onqampuliwe awugciniwe"</string> + <string name="no_albums_alert" msgid="3459550423604532470">"Awekho ama-albhamu atholakalayo"</string> + <string name="empty_album" msgid="6307897398825514762">"Azikho izithombe/amavidiyo atholakalayo"</string> + <string name="picasa_web_albums" msgid="5167008066827481663">"Ama-Albhamu Ewebhu ye-Picasa"</string> + <!-- no translation found for picasa_posts (1055151689217481993) --> + <skip /> + <string name="make_available_offline" msgid="5157950985488297112">"Yenza kutholakale ungaxhumekile kwi-inthanethi"</string> + <!-- no translation found for sync_picasa_albums (8522572542111169872) --> + <skip /> + <string name="done" msgid="217672440064436595">"Kwenziwe"</string> + <string name="sequence_in_set" msgid="7235465319919457488">"izintwana ezingu-%1$d kwezingu-%2$d:"</string> + <string name="title" msgid="7622928349908052569">"Isihloko"</string> + <string name="description" msgid="3016729318096557520">"Incazelo"</string> + <string name="time" msgid="1367953006052876956">"Isikhathi"</string> + <string name="location" msgid="3432705876921618314">"Indawo"</string> + <string name="path" msgid="4725740395885105824">"Indlela"</string> + <string name="width" msgid="9215847239714321097">"Ububanzi"</string> + <string name="height" msgid="3648885449443787772">"Ubude"</string> + <string name="orientation" msgid="4958327983165245513">"Ukujikeleza"</string> + <string name="duration" msgid="8160058911218541616">"Ubude besikhathi"</string> + <string name="mimetype" msgid="3518268469266183548">"Uhlobo lwe-MIME"</string> + <string name="file_size" msgid="4670384449129762138">"Usayizi Wefayela"</string> + <string name="maker" msgid="7921835498034236197">"Umenzi"</string> + <string name="model" msgid="8240207064064337366">"Imodili"</string> + <string name="flash" msgid="2816779031261147723">"Ifuleshi"</string> + <string name="aperture" msgid="5920657630303915195">"Imbobo"</string> + <string name="focal_length" msgid="1291383769749877010">"Ubude Befokasi"</string> + <string name="white_balance" msgid="8122534414851280901">"Ukulingana Okumhlophe"</string> + <string name="exposure_time" msgid="3146642210127439553">"Isikhathi Esisobala"</string> + <string name="iso" msgid="5028296664327335940">"i-ISO"</string> + <string name="unit_mm" msgid="1125768433254329136">"mm"</string> + <string name="manual" msgid="6608905477477607865">"Ngokulawulwa"</string> + <string name="auto" msgid="4296941368722892821">"Okuzenzakalelayo"</string> + <string name="flash_on" msgid="7891556231891837284">"Ifuleshi iqhafaziwe"</string> + <string name="flash_off" msgid="1445443413822680010">"Ayikho ifuleshi"</string> + <!-- no translation found for make_albums_available_offline:one (2955975726887896888) --> + <!-- no translation found for make_albums_available_offline:other (6929905722448632886) --> + <!-- no translation found for try_to_set_local_album_available_offline (2184754031896160755) --> + <skip /> + <!-- no translation found for set_label_all_albums (3507256844918130594) --> + <skip /> + <!-- no translation found for set_label_local_albums (5227548825039781) --> + <skip /> + <!-- no translation found for set_label_mtp_devices (5779788799122828528) --> + <skip /> + <!-- no translation found for set_label_picasa_albums (2736308697306982589) --> + <skip /> + <!-- no translation found for free_space_format (8766337315709161215) --> + <skip /> + <!-- no translation found for size_below (2074956730721942260) --> + <skip /> + <!-- no translation found for size_above (5324398253474104087) --> + <skip /> + <!-- no translation found for size_between (8779660840898917208) --> + <skip /> + <!-- no translation found for Import (3985447518557474672) --> + <skip /> + <!-- no translation found for import_complete (1098450310074640619) --> + <skip /> + <!-- no translation found for import_fail (5205927625132482529) --> + <skip /> + <!-- no translation found for camera_connected (6984353643349303075) --> + <skip /> + <!-- no translation found for camera_disconnected (3683036560562699311) --> + <skip /> + <!-- no translation found for click_import (6407959065464291972) --> + <skip /> + <!-- no translation found for widget_type_album (3245149644830731121) --> + <skip /> + <!-- no translation found for widget_type_shuffle (8594622705019763768) --> + <skip /> + <!-- no translation found for widget_type_photo (8384174698965738770) --> + <skip /> + <!-- no translation found for widget_type (7308564524449340985) --> + <skip /> + <!-- no translation found for slideshow_dream_name (6915963319933437083) --> + <skip /> + <!-- no translation found for cache_status_title (8414708919928621485) --> + <skip /> + <!-- no translation found for cache_status (7690438435538533106) --> + <skip /> + <!-- no translation found for cache_done (9194449192869777483) --> + <skip /> + <!-- no translation found for albums (7320787705180057947) --> + <skip /> + <string name="times" msgid="2023033894889499219">"Izikhathi"</string> + <!-- no translation found for locations (6649297994083130305) --> + <skip /> + <!-- no translation found for people (4114003823747292747) --> + <skip /> + <!-- no translation found for tags (5539648765482935955) --> + <skip /> + <string name="group_by" msgid="4308299657902209357">"Qoqa nge-"</string> + <!-- no translation found for settings (1534847740615665736) --> + <skip /> + <!-- no translation found for prefs_accounts (7942761992713671670) --> + <skip /> + <!-- no translation found for prefs_data_usage (410592732727343215) --> + <skip /> + <!-- no translation found for prefs_auto_upload (2467627128066665126) --> + <skip /> + <!-- no translation found for prefs_other_settings (6034181851440646681) --> + <skip /> + <!-- no translation found for about_gallery (8667445445883757255) --> + <skip /> + <!-- no translation found for sync_on_wifi_only (5795753226259399958) --> + <skip /> + <!-- no translation found for helptext_auto_upload (133741242503097377) --> + <skip /> + <!-- no translation found for enable_auto_upload (1586329406342131) --> + <skip /> + <!-- no translation found for photo_sync_is_on (1653898269297050634) --> + <skip /> + <!-- no translation found for photo_sync_is_off (6464193461664544289) --> + <skip /> + <!-- no translation found for helptext_photo_sync (8617245939103545623) --> + <skip /> + <!-- no translation found for view_photo_for_account (5608040380422337939) --> + <skip /> + <!-- no translation found for add_account (4271217504968243974) --> + <skip /> + <!-- no translation found for auto_upload_chooser_title (1494524693870792948) --> + <skip /> +</resources> diff --git a/res/values/dimensions.xml b/res/values/dimensions.xml new file mode 100644 index 000000000..90c3064d7 --- /dev/null +++ b/res/values/dimensions.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<resources> + <dimen name="appwidget_width">146dp</dimen> + <dimen name="appwidget_height">146dp</dimen> + <dimen name="stack_photo_width">140dp</dimen> + <dimen name="stack_photo_height">110dp</dimen> + + <!-- configuration for album set page --> + <dimen name="albumset_display_item_size">80dp</dimen> + <dimen name="albumset_slot_width">135dp</dimen> + <dimen name="albumset_slot_height">135dp</dimen> + <dimen name="albumset_label_font_size">11dp</dimen> + <dimen name="albumset_label_offset_y">70dp</dimen> + <dimen name="albumset_label_margin">10dp</dimen> + + <!-- configuration for album page --> + <dimen name="album_display_item_size">108dp</dimen> + <dimen name="album_slot_width">122dp</dimen> + <dimen name="album_slot_height">122dp</dimen> + + <!-- configuration for manage page --> + <dimen name="cache_bar_height">32dp</dimen> + <dimen name="cache_bar_pin_left_margin">10dp</dimen> + <dimen name="cache_bar_pin_right_margin">6dp</dimen> + <dimen name="cache_bar_button_right_margin">6dp</dimen> + <dimen name="cache_bar_font_size">12dp</dimen> + + <!-- configuration for film strip in photo page --> + <dimen name="filmstrip_top_margin">12dp</dimen> + <dimen name="filmstrip_mid_margin">0dp</dimen> + <dimen name="filmstrip_bottom_margin">2dp</dimen> + <dimen name="filmstrip_thumb_size">48dp</dimen> + <dimen name="filmstrip_content_size">56dp</dimen> + <dimen name="filmstrip_grip_size">10dp</dimen> + <dimen name="filmstrip_bar_size">10dp</dimen> + <dimen name="filmstrip_grip_width">96dp</dimen> + +</resources> diff --git a/res/values/ids.xml b/res/values/ids.xml new file mode 100644 index 000000000..b6fda4771 --- /dev/null +++ b/res/values/ids.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* +* 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. +*/ +--> +<resources> + <item type="id" name="action_toggle_full_caching" /> +</resources> diff --git a/res/values/strings.xml b/res/values/strings.xml new file mode 100644 index 000000000..cb8ce6d63 --- /dev/null +++ b/res/values/strings.xml @@ -0,0 +1,470 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2007 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. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="app_name">Gallery</string> + <!-- Title for picture frame gadget to show in list of all available gadgets --> + <string name="gadget_title">Picture frame</string> + + <!-- Used to format short video duration in Details dialog. minutes:seconds e.g. 00:30 --> + <string name="details_ms">%1$02d:%2$02d</string> + <!-- Used to format video duration in Details dialog. hours:minutes:seconds e.g. 0:21:30 --> + <string name="details_hms">%1$d:%2$02d:%3$02d</string> + <!-- Activity label. This might show up in the activity-picker --> + <string name="movie_view_label">Video player</string> + <!-- shown in the video player view while the video is being loaded, before it starts playing --> + <string name="loading_video">Loading video\u2026</string> + <string name="loading_image">Loading image\u2026</string> + + <!-- Message shown on the progress dialog to indicate we're loading the + account info [CHAR LIMIT=30] --> + <string name="loading_account">Loading account\u2026</string> + + <!-- Movie View Resume Playing dialog title --> + <string name="resume_playing_title">Resume video</string> + + <!-- Movie View Start Playing dialog title --> + <string name="resume_playing_message">Resume playing from %s ?</string> + <!-- Movie View Start Playing button "Resume from bookmark" --> + <string name="resume_playing_resume">Resume playing</string> + + <!-- Displayed in the title of those albums that are being loaded --> + <string name="loading">Loading\u2026</string> + + <!-- Displayed in the title of those pictures that fails to be loaded + [CHAR LIMIT=50]--> + <string name="fail_to_load">Failed to load</string> + + <!-- Displayed in place of the picture when we fail to get the thumbnail of it. + [CHAR LIMIT=50]--> + <string name="no_thumbnail">No thumbnail</string> + + <!-- Movie View Start Playing button "Beginning" --> + <string name="resume_playing_restart">Start over</string> + + <!-- Title of a menu item to indicate performing the image crop operation + [CHAR LIMIT=20] --> + <string name="crop_save_text">Ok</string> + <!-- Button indicating that the cropped image should be reverted back to the original --> + <!-- Hint that appears when cropping an image with more than one face --> + <string name="multiface_crop_help">Tap a face to begin.</string> + <!-- Toast/alert that the image is being saved to the SD card --> + <string name="saving_image">Saving picture\u2026</string> + <!-- menu pick: crop the currently selected image --> + <string name="crop_label">Crop picture</string> + <!-- Toast/alert that the face detection is being run --> + + <!-- Title prompted for user to choose a photo item [CHAR LIMIT=20] --> + <string name="select_image">Select photo</string> + <!-- Title prompted for user to choose a video item [CHAR LIMIT=20] --> + <string name="select_video">Select video</string> + <!-- Title prompted for user to choose a media object [CHAR LIMIT=20] --> + <string name="select_item">Select item(s)</string> + <!-- Title prompted for user to choose an album [CHAR LIMIT=20] --> + <string name="select_album">Select album(s)</string> + <!-- Title prompted for user to choose a group [CHAR LIMIT=20] --> + <string name="select_group">Select group(s)</string> + + <!-- Displayed in the title of the dialog for things to do with a picture + that is to be "set as" (e.g. set as contact photo or set as wallpaper) --> + <string name="set_image">Set picture as</string> + <!-- Toast/alert after saving wallpaper --> + <string name="wallpaper">Setting wallpaper, please wait\u2026</string> + <string name="camera_setas_wallpaper">Wallpaper</string> + + <!-- Details dialog "OK" button. Dismisses dialog. --> + <string name="delete">Delete</string> + <string name="confirm_delete">Confirm Delete</string> + <string name="cancel">Cancel</string> + <string name="share">Share</string> + + <!-- String indicating more actions are available --> + <string name="select_all">Select All</string> + <string name="deselect_all">Deselect All</string> + <string name="slideshow">Slideshow</string> + + <string name="details">Details</string> + + <!-- Title of a menu item to switch from Gallery to Camera app [CHAR LIMIT=30] --> + <string name="switch_to_camera">Switch to Camera</string> + + <!-- String indicating how many media item(s) is(are) selected + eg. 1 selected [CHAR LIMIT=30] --> + <plurals name="number_of_items_selected"> + <item quantity="zero">%1$d selected</item> + <item quantity="one">%1$d selected</item> + <item quantity="other">%1$d selected</item> + </plurals> + + <!-- String indicating how many media album(s) is(are) selected + eg. 1 selected [CHAR LIMIT=30] --> + <plurals name="number_of_albums_selected"> + <item quantity="zero">%1$d selected</item> + <item quantity="one">%1$d selected</item> + <item quantity="other">%1$d selected</item> + </plurals> + + <!-- String indicating how many media group(s) is(are) selected + eg. 1 selected [CHAR LIMIT=30] --> + <plurals name="number_of_groups_selected"> + <item quantity="zero">%1$d selected</item> + <item quantity="one">%1$d selected</item> + <item quantity="other">%1$d selected</item> + </plurals> + + <!-- String indicating timestamp of photo or video --> + <string name="show_on_map">Show on map</string> + <string name="rotate_left">Rotate Left</string> + <string name="rotate_right">Rotate Right</string> + + <!-- Toast message prompted when the specified item is not found [CHAR LIMIT=40]--> + <string name="no_such_item">Item not found</string> + + <!-- String used as a menu label. The suer can choose to edit the image + [CHAR_LIMIT=20]--> + <string name="edit">Edit</string> + + <!-- String used in a toast message indicating there is no application + available to handle a request [CHAR LIMIT=50] --> + <string name="activity_not_found">No application available</string> + + <!-- String used as a title of a progress dialog. The user can + choose to cache some Picasa picture albums on device, so it can + be viewed offline. This string is shown when the request is being + processed. [CHAR LIMIT=50] --> + <string name="process_caching_requests">Process Caching Requests</string> + + <!-- String used as a small notification label above a Picasa album. + It means the pictures of the Picasa album is currently being + transferred to local storage, so the pictures can later be viewed + offline. [CHAR LIMIT=15] --> + <string name="caching_label">Caching...</string> + + <string name="crop">Crop</string> + <string name="set_as">Set as</string> + + <!-- String indicating an approximate location eg. Around Palo Alto, CA --> + <string name="video_err">Unable to play video</string> + + <!-- Strings for grouping operations in the menu. The photos can be grouped + by their location, taken time, or tags. --> + <!-- The title of the menu item to let user choose the grouping rule, when + pressed, a submenu will shown and user can choose one grouping rule + from the submenu. --> + + <!-- Title of a menu item to group photo by location [CHAR LIMIT=30] --> + <string name="group_by_location">By location</string> + + <!-- Title of a menu tiem to group photo by taken date [CHAR LIMIT=30]--> + <string name="group_by_time">By time</string> + + <!-- Title of a menu item to group photo by tags [CHAR LIMIT=30]--> + <string name="group_by_tags">By tags</string> + + <!-- Title of a menu item to group photo by faces [CHAR LIMIT=30]--> + <string name="group_by_faces">By people</string> + + <!-- Title of a menu item to group photo by albums [CHAR LIMIT=30]--> + <string name="group_by_album">By album</string> + + <!-- Title of a menu item to group photo by size [CHAR LIMIT=30]--> + <string name="group_by_size">By size</string> + + <!-- When grouping photos by tags, the label used for photos without tags + [CHAR LIMIT=20]--> + <string name="untagged">Untagged</string> + + <!-- When grouping photos by locations, the label used for photos that don't + have location information in them [CHAR LIMIT=20]--> + <string name="no_location">No Location</string> + + <!-- This toast message is shown when network connection is lost while doing clustering --> + <string name="no_connectivity">Some locations could not be identified due to network connectivity issues</string> + + <!-- The title of the menu item to let user choose the which portion of + the media items the user wants to see. When pressed, a submenu will + appear and user can choose one of "show images only", + "show videos only", or "show all" from the submenu. --> + + <!-- Title of a menu item to show images only [CHAR LIMIT=30]--> + <string name="show_images_only">Images only</string> + + <!-- Title of a menu item to show videos only [CHAR LIMIT=30]--> + <string name="show_videos_only">Videos only</string> + + <!-- Title of a menu item to show all (both images and videos) [CHAR LIMIT=30]--> + <string name="show_all">Images and videos</string> + + <!-- Title of the StackView AppWidget --> + <string name="appwidget_title">Photo Gallery</string> + + <!-- Text for the empty state of the StackView AppWidget [CHAR LIMIT=30] --> + <string name="appwidget_empty_text">No Photos</string> + + <!-- Toast message shown when the cropped image has been saved in the + download folder [CHAR LIMIT=50]--> + <string name="crop_saved">The cropped image has been saved in download</string> + + <!-- Toast message shown when the cropped image is not saved + [CHAR LIMIT=50]--> + <string name="crop_not_saved">The cropped image is not saved</string> + + <!-- Toast message shown when there is no albums available [CHAR LIMIT=50]--> + <string name="no_albums_alert">There are no albums available</string> + + <!-- Toast message shown when we close the AlbumPage because it is empty + [CHAR LIMIT=50] --> + <string name="empty_album">There are no images/videos available</string> + + <!-- A label indicating that we will sync with Picasaweb by a corresponding + user account. The label is shown along with other Google services, + such as Gmail, Calendar, Contacts, Books, ... ,etc. [CHAR LIMIT=30] --> + <string name="picasa_web_albums">Picasa Web Albums</string> + + <!-- Album label used to indicate the collection of PWA Buzz/Post photos --> + <string name="picasa_posts">Buzz</string> + + <!-- A label describing that the current screen is for the user to pick + some albums to be viewable offline [CHAR LIMIT=30] --> + <string name="make_available_offline">Make available offline</string> + + <!-- A label of a menu item for user to sync the content [CHAR LIMIT=30] --> + <string name="sync_picasa_albums">Refresh</string> + + <!-- A label on a button. The user clicks this button after he has + finished selection. [CHAR LIMIT=15] --> + <string name="done">Done</string> + + <!-- String indicating the sequence of currently selected item in the + media set eg. 3 of 5 items [CHAR LIMIT=30] --> + <string name="sequence_in_set">%1$d of %2$d items:</string> + <!-- Text indicating the title of a media item in details window [CHAR LIMIT=14] --> + <string name="title">Title</string> + <!-- Text indicating the description of a media item in details window [CHAR LIMIT=14] --> + <string name="description">Description</string> + <!-- Text indicating the creation time of a media item in details window [CHAR LIMIT=14] --> + <string name="time">Time</string> + <!-- Text indicating the location of a media item in details window [CHAR LIMIT=14] --> + <string name="location">Location</string> + <!-- Text indicating the path of a media item in details window [CHAR LIMIT=14] --> + <string name="path">Path</string> + <!-- Text indicating the width of a media item in details window [CHAR LIMIT=14] --> + <string name="width">Width</string> + <!-- Text indicating the height of a media item in details window [CHAR LIMIT=14] --> + <string name="height">Height</string> + <!-- Text indicating the orientation of a media item in details window [CHAR LIMIT=14] --> + <string name="orientation">Orientation</string> + <!-- Text indicating the duration of a video item in details window [CHAR LIMIT=14] --> + <string name="duration">Duration</string> + <!-- Text indicating the mime type of a media item in details window [CHAR LIMIT=14] --> + <string name="mimetype">MIME Type</string> + <!-- Text indicating the file size of a media item in details window [CHAR LIMIT=14] --> + <string name="file_size">File Size</string> + <!-- Text indicating the maker of a media item in details window [CHAR LIMIT=14] --> + <string name="maker">Maker</string> + <!-- Text indicating the model of a media item in details window [CHAR LIMIT=14] --> + <string name="model">Model</string> + <!-- Text indicating flash info of a media item in details window [CHAR LIMIT=14] --> + <string name="flash">Flash</string> + <!-- Text indicating aperture of a media item in details window [CHAR LIMIT=14] --> + <string name="aperture">Aperture</string> + <!-- Text indicating the focal length of a media item in details window [CHAR LIMIT=14] --> + <string name="focal_length">Focal Length</string> + <!-- Text indicating the white balance of a media item in details window [CHAR LIMIT=14] --> + <string name="white_balance">White Balance</string> + <!-- Text indicating the exposure time of a media item in details window [CHAR LIMIT=14] --> + <string name="exposure_time">Exposure Time</string> + <!-- Text indicating the ISO speed rating of a media item in details window [CHAR LIMIT=14] --> + <string name="iso">ISO</string> + <!-- String indicating the time units in seconds. [CHAR LIMIT=8] --> + <!-- String indicating the length units in milli-meters. [CHAR LIMIT=8] --> + <string name="unit_mm">mm</string> + <!-- String indicating how camera shooting feature is used. [CHAR LIMIT=8] --> + <string name="manual">Manual</string> + <!-- String indicating how camera shooting feature is used. [CHAR LIMIT=8] --> + <string name="auto">Auto</string> + <!-- String indicating camera flash is fired. [CHAR LIMIT=14] --> + <string name="flash_on">Flash fired</string> + <!-- String indicating camera flash is not used. [CHAR LIMIT=14] --> + <string name="flash_off">No flash</string> + + + <!-- Toast message shown after we make some album(s) available offline [CHAR LIMIT=50] --> + <plurals name="make_albums_available_offline"> + <item quantity="one">Making album available offline</item> + <item quantity="other">Making albums available offline</item> + </plurals> + + <!-- Toast message shown after we try to make a local album available offline + [CHAR LIMIT=150] --> + <string name="try_to_set_local_album_available_offline"> + This item is stored locally and available offline.</string> + + <!-- A label shown on the action bar. It indicates that the user is + viewing all available albums [CHAR LIMIT=20] --> + <string name="set_label_all_albums">All Albums</string> + + <!-- A label shown on the action bar. It indicates that the user is + viewing albums stored locally on the device [CHAR LIMIT=20] --> + <string name="set_label_local_albums">Local Albums</string> + + <!-- A label shown on the action bar. It indicates that the user is + viewing MTP devices connected (like other digital cameras). + [CHAR LIMIT=20] --> + <string name="set_label_mtp_devices">MTP Devices</string> + + <!-- A label shown on the action bar. It indicates that the user is + viewing Picasa albums [CHAR LIMIT=20] --> + <string name="set_label_picasa_albums">Picasa Albums</string> + + <!-- Label indicating the amount on free space on the device. The parameter + is a string representation of the amount of free space, eg. "20MB". + [CHAR LIMIT=20] + --> + <string name="free_space_format"><xliff:g id="bytes">%s</xliff:g> free</string> + + <!-- Label of a group of pictures. The size of each picture in this group is + less than a certain amount. The parameter is a string representation + of that amount, eg. "10MB". + [CHAR LIMIT=20] + --> + <string name="size_below"><xliff:g id="size">%1$s</xliff:g> or below</string> + + <!-- Label of a group of pictures. The size of each picture in this group is + more than a certain amount. The parameter is a string representation + of that amount, eg. "10MB". + [CHAR LIMIT=20] + --> + <string name="size_above"><xliff:g id="size">%1$s</xliff:g> or above</string> + + <!-- Label of a group of pictures. The size of each picture in this group is + between two amounts. The parameters are string representations of the two + amounts, eg. "10MB", "100MB". + [CHAR LIMIT=20] + --> + <string name="size_between"><xliff:g id="min_size">%1$s</xliff:g> to <xliff:g id="max_size">%2$s</xliff:g></string> + + <!-- A label shown on the action bar. It indicates that the operation + to import media item(s) [CHAR LIMIT=20] --> + <string name="Import">Import</string> + + <!-- A label shown on the action bar. It indicates whether the import + operation succeeds or fails. [CHAR LIMIT=20] --> + <string name="import_complete">Import Complete</string> + <string name="import_fail">Import Fail</string> + + <!-- A toast indicating a camera is connected to the device [CHAR LIMIT=30]--> + <string name="camera_connected">Camera connected</string> + <!-- A toast indicating a camera is disconnected [CHAR LIMIT=30] --> + <string name="camera_disconnected">Camera disconnected</string> + <!-- A label shown on MTP albums thumbnail to instruct users to import + [CHAR LIMIT=40] --> + <string name="click_import">Touch here to import</string> + + <!-- The label on the radio button for the widget type that shows the images randomly. [CHAR LIMIT=30]--> + <string name="widget_type_album">Images from an album</string> + <!-- The label on the radio button for the widget type that shows the images in an album. [CHAR LIMIT=30]--> + <string name="widget_type_shuffle">Shuffle all images</string> + <!-- The label on the radio button for the widget type that shows only one image. [CHAR LIMIT=30]--> + <string name="widget_type_photo">Pick an image</string> + + <!-- The title of the dialog for choosing the type of widget. [CHAR LIMIT=20] --> + <string name="widget_type">Widget Type</string> + + <!-- Title of the Android Dreams slideshow screensaver. [CHAR LIMIT=20] --> + <string name="slideshow_dream_name">Slideshow</string> + + <!-- The title of the picasa's caching notification. [CHAR LIMIT=40] --> + <string name="cache_status_title">Prefetching picasa photos:</string> + + <!-- The current download status in the caching notification. [CHAR LIMIT=40] --> + <string name="cache_status">Download <xliff:g id="number">%1$s</xliff:g> of <xliff:g id="number">%2$s</xliff:g> photos</string> + + <!-- Indicate complete status, picasa's caching notification. [CHAR LIMIT=40] --> + <string name="cache_done">Download complete</string> + + <!-- Group by Albums tab on Action Bar. [CHAR LIMIT=12] --> + <string name="albums">Albums</string> + + <!-- Group by Times tab on Action Bar. [CHAR LIMIT=12] --> + <string name="times">Times</string> + + <!-- Group by Locations tab on Action Bar. [CHAR LIMIT=12] --> + <string name="locations">Locations</string> + + <!-- Group by People tab on Action Bar. [CHAR LIMIT=12] --> + <string name="people">People</string> + + <!-- Group by Tags tab on Action Bar. [CHAR LIMIT=12] --> + <string name="tags">Tags</string> + + <!-- Group by menu item. [CHAR LIMIT=20] --> + <string name="group_by">Group by</string> + + <!-- The strings used in Gallery Settings --> + + <!-- The title of the menu item which enable the settings [CHAR LIMIT=20] --> + <string name="settings">Settings</string> + + <!-- The header of the preference group about account [CHAR LIMIT=40] --> + <string name="prefs_accounts">Account settings</string> + + <!-- The header of the preference group about data usage [CHAR LIMIT=40] --> + <string name="prefs_data_usage">Data usage settings</string> + + <!-- The header of the preference group about Auto-upload [CHAR LIMIT=40] --> + <string name="prefs_auto_upload">Auto-upload</string> + + <!-- The header of the preference group about other settings [CHAR LIMIT=40] --> + <string name="prefs_other_settings">Other settings</string> + + <!-- The title of the preference item which shows details info about the + gallery [CHAR LIMIT=40] --> + <string name="about_gallery">About Gallery</string> + + <!-- The title of the preference item which controls whether we do sync + only on wifi network [CHAR LIMIT=40] --> + <string name="sync_on_wifi_only">Sync on WiFi only</string> + + <!-- The help document about auto upload [CHAR LIMIT=120] --> + <string name="helptext_auto_upload">Automatically upload all the photos and videos you take to a private picasa web album</string> + + <!-- The title of the preference item which sets whether auto-upload is + enabled [CHAR LIMIT=40] --> + <string name="enable_auto_upload">Enable Auto-upload</string> + + <!-- The title which indicates the picasa sync for the account is on [CHAR LIMIT=30] --> + <string name="photo_sync_is_on">Google photos sync is ON</string> + + <!-- The title which indicates the picasa sync for the account is off [CHAR LIMIT=30] --> + <string name="photo_sync_is_off">Google photos sync is OFF</string> + + <!-- The help document which explains user can change system sync settings + by this preference item [CHAR LIMIT=40] --> + <string name="helptext_photo_sync">Change sync preferences or remove this account</string> + + <!-- The title of the preference item which sets whether the photos of + the account are visible in Gallery [CHAR LIMIT=60] --> + <string name="view_photo_for_account">View photos and videos from this account in the Gallery</string> + + <!-- The title of menu item where user can add a new account --> + <string name="add_account">Add account</string> + + <!-- The title of the dialog for the user to choose the account for auto + uploading [CHAR LIMIT=30] --> + <string name="auto_upload_chooser_title">Choose Auto-upload account</string> + +</resources> diff --git a/res/values/styles.xml b/res/values/styles.xml new file mode 100644 index 000000000..7da37df98 --- /dev/null +++ b/res/values/styles.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <style name="Theme.Gallery" parent="android:Theme.Holo"> + <item name="android:displayOptions"></item> + <item name="android:windowFullscreen">true</item> + <item name="android:windowContentOverlay">@null</item> + <item name="android:actionBarStyle">@style/Holo.ActionBar</item> + </style> + <style name="Holo.ActionBar" parent="android:Widget.Holo.ActionBar"> + <item name="android:background">@drawable/actionbar_translucent</item> + </style> + <style name="MediaButton.Play" parent="@android:style/MediaButton.Play"> + <item name="android:background">@null</item> + <item name="android:src">@drawable/icn_media_play</item> + </style> + <style name="DialogPickerTheme" parent="android:Theme.Holo.Dialog"> + <item name="android:windowBackground">@android:color/transparent</item> + <item name="android:windowIsFloating">false</item> + </style> +</resources> diff --git a/res/xml/device_filter.xml b/res/xml/device_filter.xml new file mode 100644 index 000000000..36cd13da7 --- /dev/null +++ b/res/xml/device_filter.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<resources> + <!-- filter for PTP devices --> + <usb-device class="6" subclass="1" protocol="1" /> +</resources> diff --git a/res/xml/gallery_settings.xml b/res/xml/gallery_settings.xml new file mode 100644 index 000000000..8490454ad --- /dev/null +++ b/res/xml/gallery_settings.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> + <PreferenceCategory + android:key="prefs_account_settings" + android:title="@string/prefs_accounts"/> + <PreferenceCategory + android:title="@string/prefs_data_usage"> + <CheckBoxPreference + android:key="prefs_sync_on_wifi_only" + android:title="@string/sync_on_wifi_only" /> + </PreferenceCategory> + <PreferenceCategory + android:key="prefs_auto_upload_settings" + android:title="@string/prefs_auto_upload"> + <com.android.gallery3d.settings.AutoUploadHelpTextPreference /> + <CheckBoxPreference + android:key="prefs_auto_upload_enabled" + android:title="@string/enable_auto_upload"/> + </PreferenceCategory> + <PreferenceCategory + android:title="@string/prefs_other_settings"> + <Preference + android:key="prefs_about_gallery" + android:title="@string/about_gallery"/> + </PreferenceCategory> +</PreferenceScreen> diff --git a/res/xml/syncadapter.xml b/res/xml/syncadapter.xml new file mode 100644 index 000000000..d60bd5a96 --- /dev/null +++ b/res/xml/syncadapter.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2009, 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. + */ +--> + +<!-- The attributes in this XML file provide configuration information --> +<!-- for the SyncAdapter. --> + +<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android" + android:contentAuthority="com.android.gallery3d.picasa.contentprovider" + android:accountType="com.google" +/> diff --git a/res/xml/wallpaper_picker_preview.xml b/res/xml/wallpaper_picker_preview.xml new file mode 100644 index 000000000..759ff6fd4 --- /dev/null +++ b/res/xml/wallpaper_picker_preview.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2009 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. +--> + +<wallpaper-preview xmlns:android="http://schemas.android.com/apk/res/android" + android:staticWallpaperPreview="@drawable/wallpaper_picker_preview"> +</wallpaper-preview> diff --git a/res/xml/widget_info.xml b/res/xml/widget_info.xml new file mode 100644 index 000000000..5f71192bd --- /dev/null +++ b/res/xml/widget_info.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" + android:minWidth="220dp" + android:minHeight="220dp" + android:updatePeriodMillis="86400000" + android:previewImage="@drawable/preview" + android:initialLayout="@layout/appwidget_main" + android:configure="com.android.gallery3d.widget.WidgetConfigure"/> diff --git a/src/com/android/gallery3d/anim/AlphaAnimation.java b/src/com/android/gallery3d/anim/AlphaAnimation.java new file mode 100644 index 000000000..cb17527b8 --- /dev/null +++ b/src/com/android/gallery3d/anim/AlphaAnimation.java @@ -0,0 +1,48 @@ +/* + * 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.anim; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.ui.GLCanvas; + +public class AlphaAnimation extends CanvasAnimation { + private final float mStartAlpha; + private final float mEndAlpha; + private float mCurrentAlpha; + + public AlphaAnimation(float from, float to) { + mStartAlpha = from; + mEndAlpha = to; + mCurrentAlpha = from; + } + + @Override + public void apply(GLCanvas canvas) { + canvas.multiplyAlpha(mCurrentAlpha); + } + + @Override + public int getCanvasSaveFlags() { + return GLCanvas.SAVE_FLAG_ALPHA; + } + + @Override + protected void onCalculate(float progress) { + mCurrentAlpha = Utils.clamp(mStartAlpha + + (mEndAlpha - mStartAlpha) * progress, 0f, 1f); + } +} diff --git a/src/com/android/gallery3d/anim/Animation.java b/src/com/android/gallery3d/anim/Animation.java new file mode 100644 index 000000000..bd5a6cd72 --- /dev/null +++ b/src/com/android/gallery3d/anim/Animation.java @@ -0,0 +1,92 @@ +/* + * 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.anim; + +import com.android.gallery3d.common.Utils; + +import android.view.animation.Interpolator; + +// Animation calculates a value according to the current input time. +// +// 1. First we need to use setDuration(int) to set the duration of the +// animation. The duration is in milliseconds. +// 2. Then we should call start(). The actual start time is the first value +// passed to calculate(long). +// 3. Each time we want to get an animation value, we call +// calculate(long currentTimeMillis) to ask the Animation to calculate it. +// The parameter passed to calculate(long) should be nonnegative. +// 4. Use get() to get that value. +// +// In step 3, onCalculate(float progress) is called so subclasses can calculate +// the value according to progress (progress is a value in [0,1]). +// +// Before onCalculate(float) is called, There is an optional interpolator which +// can change the progress value. The interpolator can be set by +// setInterpolator(Interpolator). If the interpolator is used, the value passed +// to onCalculate may be (for example, the overshoot effect). +// +// The isActive() method returns true after the animation start() is called and +// before calculate is passed a value which reaches the duration of the +// animation. +// +// The start() method can be called again to restart the Animation. +// +abstract public class Animation { + private static final long ANIMATION_START = -1; + private static final long NO_ANIMATION = -2; + + private long mStartTime = NO_ANIMATION; + private int mDuration; + private Interpolator mInterpolator; + + public void setInterpolator(Interpolator interpolator) { + mInterpolator = interpolator; + } + + public void setDuration(int duration) { + mDuration = duration; + } + + public void start() { + mStartTime = ANIMATION_START; + } + + public void setStartTime(long time) { + mStartTime = time; + } + + public boolean isActive() { + return mStartTime != NO_ANIMATION; + } + + public void forceStop() { + mStartTime = NO_ANIMATION; + } + + public boolean calculate(long currentTimeMillis) { + if (mStartTime == NO_ANIMATION) return false; + if (mStartTime == ANIMATION_START) mStartTime = currentTimeMillis; + int elapse = (int) (currentTimeMillis - mStartTime); + float x = Utils.clamp((float) elapse / mDuration, 0f, 1f); + Interpolator i = mInterpolator; + onCalculate(i != null ? i.getInterpolation(x) : x); + if (elapse >= mDuration) mStartTime = NO_ANIMATION; + return mStartTime != NO_ANIMATION; + } + + abstract protected void onCalculate(float progress); +} diff --git a/src/com/android/gallery3d/anim/AnimationSet.java b/src/com/android/gallery3d/anim/AnimationSet.java new file mode 100644 index 000000000..773cb4314 --- /dev/null +++ b/src/com/android/gallery3d/anim/AnimationSet.java @@ -0,0 +1,76 @@ +/* + * 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.anim; + +import com.android.gallery3d.ui.GLCanvas; + +import java.util.ArrayList; + +public class AnimationSet extends CanvasAnimation { + + private final ArrayList<CanvasAnimation> mAnimations = + new ArrayList<CanvasAnimation>(); + private int mSaveFlags = 0; + + + public void addAnimation(CanvasAnimation anim) { + mAnimations.add(anim); + mSaveFlags |= anim.getCanvasSaveFlags(); + } + + @Override + public void apply(GLCanvas canvas) { + for (int i = 0, n = mAnimations.size(); i < n; i++) { + mAnimations.get(i).apply(canvas); + } + } + + @Override + public int getCanvasSaveFlags() { + return mSaveFlags; + } + + @Override + protected void onCalculate(float progress) { + // DO NOTHING + } + + @Override + public boolean calculate(long currentTimeMillis) { + boolean more = false; + for (CanvasAnimation anim : mAnimations) { + more |= anim.calculate(currentTimeMillis); + } + return more; + } + + @Override + public void start() { + for (CanvasAnimation anim : mAnimations) { + anim.start(); + } + } + + @Override + public boolean isActive() { + for (CanvasAnimation anim : mAnimations) { + if (anim.isActive()) return true; + } + return false; + } + +} diff --git a/src/com/android/gallery3d/anim/CanvasAnimation.java b/src/com/android/gallery3d/anim/CanvasAnimation.java new file mode 100644 index 000000000..4c8bcc825 --- /dev/null +++ b/src/com/android/gallery3d/anim/CanvasAnimation.java @@ -0,0 +1,25 @@ +/* + * 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.anim; + +import com.android.gallery3d.ui.GLCanvas; + +public abstract class CanvasAnimation extends Animation { + + public abstract int getCanvasSaveFlags(); + public abstract void apply(GLCanvas canvas); +} diff --git a/src/com/android/gallery3d/anim/FloatAnimation.java b/src/com/android/gallery3d/anim/FloatAnimation.java new file mode 100644 index 000000000..1294ec2f4 --- /dev/null +++ b/src/com/android/gallery3d/anim/FloatAnimation.java @@ -0,0 +1,40 @@ +/* + * 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.anim; + +public class FloatAnimation extends Animation { + + private final float mFrom; + private final float mTo; + private float mCurrent; + + public FloatAnimation(float from, float to, int duration) { + mFrom = from; + mTo = to; + mCurrent = from; + setDuration(duration); + } + + @Override + protected void onCalculate(float progress) { + mCurrent = mFrom + (mTo - mFrom) * progress; + } + + public float get() { + return mCurrent; + } +} diff --git a/src/com/android/gallery3d/app/AbstractGalleryActivity.java b/src/com/android/gallery3d/app/AbstractGalleryActivity.java new file mode 100644 index 000000000..d0d7b0fad --- /dev/null +++ b/src/com/android/gallery3d/app/AbstractGalleryActivity.java @@ -0,0 +1,198 @@ +/* + * 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.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.ImageCacheService; +import com.android.gallery3d.ui.GLRoot; +import com.android.gallery3d.ui.GLRootView; +import com.android.gallery3d.ui.PositionRepository; +import com.android.gallery3d.util.ThreadPool; + +import android.app.ActionBar; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; + +public class AbstractGalleryActivity extends Activity implements GalleryActivity { + @SuppressWarnings("unused") + private static final String TAG = "AbstractGalleryActivity"; + private GLRootView mGLRootView; + private StateManager mStateManager; + private PositionRepository mPositionRepository = new PositionRepository(); + + private AlertDialog mAlertDialog = null; + private BroadcastReceiver mMountReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (getExternalCacheDir() != null) onStorageReady(); + } + }; + private IntentFilter mMountFilter = new IntentFilter(Intent.ACTION_MEDIA_MOUNTED); + + @Override + protected void onSaveInstanceState(Bundle outState) { + mGLRootView.lockRenderThread(); + try { + super.onSaveInstanceState(outState); + getStateManager().saveState(outState); + } finally { + mGLRootView.unlockRenderThread(); + } + } + + public Context getAndroidContext() { + return this; + } + + public ImageCacheService getImageCacheService() { + return ((GalleryApp) getApplication()).getImageCacheService(); + } + + public DataManager getDataManager() { + return ((GalleryApp) getApplication()).getDataManager(); + } + + public ThreadPool getThreadPool() { + return ((GalleryApp) getApplication()).getThreadPool(); + } + + public GalleryApp getGalleryApplication() { + return (GalleryApp) getApplication(); + } + + public synchronized StateManager getStateManager() { + if (mStateManager == null) { + mStateManager = new StateManager(this); + } + return mStateManager; + } + + public GLRoot getGLRoot() { + return mGLRootView; + } + + public PositionRepository getPositionRepository() { + return mPositionRepository; + } + + @Override + public void setContentView(int resId) { + super.setContentView(resId); + mGLRootView = (GLRootView) findViewById(R.id.gl_root_view); + } + + public int getActionBarHeight() { + ActionBar actionBar = getActionBar(); + return actionBar != null ? actionBar.getHeight() : 0; + } + + protected void onStorageReady() { + if (mAlertDialog != null) { + mAlertDialog.dismiss(); + mAlertDialog = null; + unregisterReceiver(mMountReceiver); + } + } + + @Override + protected void onStart() { + super.onStart(); + if (getExternalCacheDir() == null) { + OnCancelListener onCancel = new OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + finish(); + } + }; + OnClickListener onClick = new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }; + mAlertDialog = new AlertDialog.Builder(this) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle("No Storage") + .setMessage("No external storage available.") + .setNegativeButton(android.R.string.cancel, onClick) + .setOnCancelListener(onCancel) + .show(); + registerReceiver(mMountReceiver, mMountFilter); + } + } + + @Override + protected void onStop() { + super.onStop(); + if (mAlertDialog != null) { + unregisterReceiver(mMountReceiver); + mAlertDialog.dismiss(); + mAlertDialog = null; + } + } + + @Override + protected void onResume() { + super.onResume(); + mGLRootView.lockRenderThread(); + try { + getStateManager().resume(); + getDataManager().resume(); + } finally { + mGLRootView.unlockRenderThread(); + } + mGLRootView.onResume(); + } + + @Override + protected void onPause() { + super.onPause(); + mGLRootView.onPause(); + mGLRootView.lockRenderThread(); + try { + getStateManager().pause(); + getDataManager().pause(); + } finally { + mGLRootView.unlockRenderThread(); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + mGLRootView.lockRenderThread(); + try { + getStateManager().notifyActivityResult( + requestCode, resultCode, data); + } finally { + mGLRootView.unlockRenderThread(); + } + } + + @Override + public GalleryActionBar getGalleryActionBar() { + return null; + } +} diff --git a/src/com/android/gallery3d/app/ActivityState.java b/src/com/android/gallery3d/app/ActivityState.java new file mode 100644 index 000000000..bfacc5484 --- /dev/null +++ b/src/com/android/gallery3d/app/ActivityState.java @@ -0,0 +1,136 @@ +/* + * 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.app; + +import com.android.gallery3d.ui.GLView; + +import android.app.ActionBar; +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.WindowManager; + +abstract public class ActivityState { + public static final int FLAG_HIDE_ACTION_BAR = 1; + public static final int FLAG_HIDE_STATUS_BAR = 2; + + protected GalleryActivity mActivity; + protected Bundle mData; + protected int mFlags; + + protected ResultEntry mReceivedResults; + protected ResultEntry mResult; + + protected static class ResultEntry { + public int requestCode; + public int resultCode = Activity.RESULT_CANCELED; + public Intent resultData; + ResultEntry next; + } + + protected ActivityState() { + } + + protected void setContentPane(GLView content) { + mActivity.getGLRoot().setContentPane(content); + } + + void initialize(GalleryActivity activity, Bundle data) { + mActivity = activity; + mData = data; + } + + public Bundle getData() { + return mData; + } + + protected void onBackPressed() { + mActivity.getStateManager().finishState(this); + } + + protected void setStateResult(int resultCode, Intent data) { + if (mResult == null) return; + mResult.resultCode = resultCode; + mResult.resultData = data; + } + + protected void onSaveState(Bundle outState) { + } + + protected void onStateResult(int requestCode, int resultCode, Intent data) { + } + + protected void onCreate(Bundle data, Bundle storedState) { + } + + protected void onPause() { + } + + // should only be called by StateManager + void resume() { + Activity activity = (Activity) mActivity; + ActionBar actionBar = activity.getActionBar(); + if (actionBar != null) { + if ((mFlags & FLAG_HIDE_ACTION_BAR) != 0) { + actionBar.hide(); + } else { + actionBar.show(); + } + int stateCount = mActivity.getStateManager().getStateCount(); + actionBar.setDisplayOptions( + stateCount == 1 ? 0 : ActionBar.DISPLAY_HOME_AS_UP, + ActionBar.DISPLAY_HOME_AS_UP); + } + + activity.invalidateOptionsMenu(); + + if ((mFlags & FLAG_HIDE_STATUS_BAR) != 0) { + WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes(); + params.systemUiVisibility = View.STATUS_BAR_HIDDEN; + ((Activity) mActivity).getWindow().setAttributes(params); + } else { + WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes(); + params.systemUiVisibility = View.STATUS_BAR_VISIBLE; + ((Activity) mActivity).getWindow().setAttributes(params); + } + + ResultEntry entry = mReceivedResults; + if (entry != null) { + mReceivedResults = null; + onStateResult(entry.requestCode, entry.resultCode, entry.resultData); + } + onResume(); + } + + // a subclass of ActivityState should override the method to resume itself + protected void onResume() { + } + + protected boolean onCreateActionBar(Menu menu) { + return false; + } + + protected boolean onItemSelected(MenuItem item) { + return false; + } + + protected void onDestroy() { + } +} diff --git a/src/com/android/gallery3d/app/AlbumDataAdapter.java b/src/com/android/gallery3d/app/AlbumDataAdapter.java new file mode 100644 index 000000000..9934cf88c --- /dev/null +++ b/src/com/android/gallery3d/app/AlbumDataAdapter.java @@ -0,0 +1,367 @@ +/* + * 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.app; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.ui.AlbumView; +import com.android.gallery3d.ui.SynchronizedHandler; + +import android.os.Handler; +import android.os.Message; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +public class AlbumDataAdapter implements AlbumView.Model { + @SuppressWarnings("unused") + private static final String TAG = "AlbumDataAdapter"; + private static final int DATA_CACHE_SIZE = 1000; + + private static final int MSG_LOAD_START = 1; + private static final int MSG_LOAD_FINISH = 2; + private static final int MSG_RUN_OBJECT = 3; + + private static final int MIN_LOAD_COUNT = 32; + private static final int MAX_LOAD_COUNT = 64; + + private final MediaItem[] mData; + private final long[] mItemVersion; + private final long[] mSetVersion; + + private int mActiveStart = 0; + private int mActiveEnd = 0; + + private int mContentStart = 0; + private int mContentEnd = 0; + + private final MediaSet mSource; + private long mSourceVersion = MediaObject.INVALID_DATA_VERSION; + + private final Handler mMainHandler; + private int mSize = 0; + + private AlbumView.ModelListener mModelListener; + private MySourceListener mSourceListener = new MySourceListener(); + private LoadingListener mLoadingListener; + + private ReloadTask mReloadTask; + + public AlbumDataAdapter(GalleryActivity context, MediaSet mediaSet) { + mSource = mediaSet; + + mData = new MediaItem[DATA_CACHE_SIZE]; + mItemVersion = new long[DATA_CACHE_SIZE]; + mSetVersion = new long[DATA_CACHE_SIZE]; + Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION); + Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION); + + mMainHandler = new SynchronizedHandler(context.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_RUN_OBJECT: + ((Runnable) message.obj).run(); + return; + case MSG_LOAD_START: + if (mLoadingListener != null) mLoadingListener.onLoadingStarted(); + return; + case MSG_LOAD_FINISH: + if (mLoadingListener != null) mLoadingListener.onLoadingFinished(); + return; + } + } + }; + } + + public void resume() { + mSource.addContentListener(mSourceListener); + mReloadTask = new ReloadTask(); + mReloadTask.start(); + } + + public void pause() { + mReloadTask.terminate(); + mReloadTask = null; + mSource.removeContentListener(mSourceListener); + } + + public MediaItem get(int index) { + if (!isActive(index)) { + throw new IllegalArgumentException(String.format( + "%s not in (%s, %s)", index, mActiveStart, mActiveEnd)); + } + return mData[index % mData.length]; + } + + public int getActiveStart() { + return mActiveStart; + } + + public int getActiveEnd() { + return mActiveEnd; + } + + public boolean isActive(int index) { + return index >= mActiveStart && index < mActiveEnd; + } + + public int size() { + return mSize; + } + + private void clearSlot(int slotIndex) { + mData[slotIndex] = null; + mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION; + mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION; + } + + private void setContentWindow(int contentStart, int contentEnd) { + if (contentStart == mContentStart && contentEnd == mContentEnd) return; + int end = mContentEnd; + int start = mContentStart; + + // We need change the content window before calling reloadData(...) + synchronized (this) { + mContentStart = contentStart; + mContentEnd = contentEnd; + } + MediaItem[] data = mData; + long[] itemVersion = mItemVersion; + long[] setVersion = mSetVersion; + if (contentStart >= end || start >= contentEnd) { + for (int i = start, n = end; i < n; ++i) { + clearSlot(i % DATA_CACHE_SIZE); + } + } else { + for (int i = start; i < contentStart; ++i) { + clearSlot(i % DATA_CACHE_SIZE); + } + for (int i = contentEnd, n = end; i < n; ++i) { + clearSlot(i % DATA_CACHE_SIZE); + } + } + if (mReloadTask != null) mReloadTask.notifyDirty(); + } + + public void setActiveWindow(int start, int end) { + if (start == mActiveStart && end == mActiveEnd) return; + + mActiveStart = start; + mActiveEnd = end; + + Utils.assertTrue(start <= end + && end - start <= mData.length && end <= mSize); + + int length = mData.length; + mActiveStart = start; + mActiveEnd = end; + + // If no data is visible, keep the cache content + if (start == end) return; + + int contentStart = Utils.clamp((start + end) / 2 - length / 2, + 0, Math.max(0, mSize - length)); + int contentEnd = Math.min(contentStart + length, mSize); + if (mContentStart > start || mContentEnd < end + || Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) { + setContentWindow(contentStart, contentEnd); + } + } + + private class MySourceListener implements ContentListener { + public void onContentDirty() { + if (mReloadTask != null) mReloadTask.notifyDirty(); + } + } + + public void setModelListener(AlbumView.ModelListener listener) { + mModelListener = listener; + } + + public void setLoadingListener(LoadingListener listener) { + mLoadingListener = listener; + } + + private <T> T executeAndWait(Callable<T> callable) { + FutureTask<T> task = new FutureTask<T>(callable); + mMainHandler.sendMessage( + mMainHandler.obtainMessage(MSG_RUN_OBJECT, task)); + try { + return task.get(); + } catch (InterruptedException e) { + return null; + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + private static class UpdateInfo { + public long version; + public int reloadStart; + public int reloadCount; + + public int size; + public ArrayList<MediaItem> items; + } + + private class GetUpdateInfo implements Callable<UpdateInfo> { + private final long mVersion; + + public GetUpdateInfo(long version) { + mVersion = version; + } + + public UpdateInfo call() throws Exception { + UpdateInfo info = new UpdateInfo(); + long version = mVersion; + info.version = mSourceVersion; + info.size = mSize; + long setVersion[] = mSetVersion; + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + int index = i % DATA_CACHE_SIZE; + if (setVersion[index] != version) { + info.reloadStart = i; + info.reloadCount = Math.min(MAX_LOAD_COUNT, n - i); + return info; + } + } + return mSourceVersion == mVersion ? null : info; + } + } + + private class UpdateContent implements Callable<Void> { + + private UpdateInfo mUpdateInfo; + + public UpdateContent(UpdateInfo info) { + mUpdateInfo = info; + } + + @Override + public Void call() throws Exception { + UpdateInfo info = mUpdateInfo; + mSourceVersion = info.version; + if (mSize != info.size) { + mSize = info.size; + if (mModelListener != null) mModelListener.onSizeChanged(mSize); + if (mContentEnd > mSize) mContentEnd = mSize; + if (mActiveEnd > mSize) mActiveEnd = mSize; + } + + ArrayList<MediaItem> items = info.items; + + if (items == null) return null; + int start = Math.max(info.reloadStart, mContentStart); + int end = Math.min(info.reloadStart + items.size(), mContentEnd); + + for (int i = start; i < end; ++i) { + int index = i % DATA_CACHE_SIZE; + mSetVersion[index] = info.version; + MediaItem updateItem = items.get(i - info.reloadStart); + long itemVersion = updateItem.getDataVersion(); + if (mItemVersion[index] != itemVersion) { + mItemVersion[index] = itemVersion; + mData[index] = updateItem; + if (mModelListener != null && i >= mActiveStart && i < mActiveEnd) { + mModelListener.onWindowContentChanged(i); + } + } + } + return null; + } + } + + /* + * The thread model of ReloadTask + * * + * [Reload Task] [Main Thread] + * | | + * getUpdateInfo() --> | (synchronous call) + * (wait) <---- getUpdateInfo() + * | | + * Load Data | + * | | + * updateContent() --> | (synchronous call) + * (wait) updateContent() + * | | + * | | + */ + private class ReloadTask extends Thread { + + private volatile boolean mActive = true; + private volatile boolean mDirty = true; + private boolean mIsLoading = false; + + private void updateLoading(boolean loading) { + if (mIsLoading == loading) return; + mIsLoading = loading; + mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH); + } + + @Override + public void run() { + boolean updateComplete = false; + while (mActive) { + synchronized (this) { + if (mActive && !mDirty && updateComplete) { + updateLoading(false); + Utils.waitWithoutInterrupt(this); + continue; + } + } + mDirty = false; + updateLoading(true); + long version; + synchronized (DataManager.LOCK) { + version = mSource.reload(); + } + UpdateInfo info = executeAndWait(new GetUpdateInfo(version)); + updateComplete = info == null; + if (updateComplete) continue; + synchronized (DataManager.LOCK) { + if (info.version != version) { + info.size = mSource.getMediaItemCount(); + info.version = version; + } + if (info.reloadCount > 0) { + info.items = mSource.getMediaItem(info.reloadStart, info.reloadCount); + } + } + executeAndWait(new UpdateContent(info)); + } + updateLoading(false); + } + + public synchronized void notifyDirty() { + mDirty = true; + notifyAll(); + } + + public synchronized void terminate() { + mActive = false; + notifyAll(); + } + } +} diff --git a/src/com/android/gallery3d/app/AlbumPage.java b/src/com/android/gallery3d/app/AlbumPage.java new file mode 100644 index 000000000..5c09ce2d2 --- /dev/null +++ b/src/com/android/gallery3d/app/AlbumPage.java @@ -0,0 +1,602 @@ +/* + * 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.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaDetails; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.MtpDevice; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.ActionModeHandler; +import com.android.gallery3d.ui.ActionModeHandler.ActionModeListener; +import com.android.gallery3d.ui.AlbumView; +import com.android.gallery3d.ui.DetailsWindow; +import com.android.gallery3d.ui.DetailsWindow.CloseListener; +import com.android.gallery3d.ui.GLCanvas; +import com.android.gallery3d.ui.GLView; +import com.android.gallery3d.ui.GridDrawer; +import com.android.gallery3d.ui.HighlightDrawer; +import com.android.gallery3d.ui.PositionProvider; +import com.android.gallery3d.ui.PositionRepository; +import com.android.gallery3d.ui.PositionRepository.Position; +import com.android.gallery3d.ui.SelectionManager; +import com.android.gallery3d.ui.SlotView; +import com.android.gallery3d.ui.StaticBackground; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.GalleryUtils; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.provider.MediaStore; +import android.view.ActionMode; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View.MeasureSpec; +import android.widget.Toast; + +import java.util.Random; + +public class AlbumPage extends ActivityState implements GalleryActionBar.ClusterRunner, + SelectionManager.SelectionListener { + @SuppressWarnings("unused") + private static final String TAG = "AlbumPage"; + + public static final String KEY_MEDIA_PATH = "media-path"; + public static final String KEY_SET_CENTER = "set-center"; + public static final String KEY_AUTO_SELECT_ALL = "auto-select-all"; + public static final String KEY_SHOW_CLUSTER_MENU = "cluster-menu"; + + private static final int REQUEST_SLIDESHOW = 1; + private static final int REQUEST_PHOTO = 2; + private static final int REQUEST_DO_ANIMATION = 3; + + private static final float USER_DISTANCE_METER = 0.3f; + + private boolean mIsActive = false; + private StaticBackground mStaticBackground; + private AlbumView mAlbumView; + private Path mMediaSetPath; + + private AlbumDataAdapter mAlbumDataAdapter; + + protected SelectionManager mSelectionManager; + private GridDrawer mGridDrawer; + private HighlightDrawer mHighlightDrawer; + + private boolean mGetContent; + private boolean mShowClusterMenu; + + private ActionMode mActionMode; + private ActionModeHandler mActionModeHandler; + private int mFocusIndex = 0; + private DetailsWindow mDetailsWindow; + private MediaSet mMediaSet; + private boolean mShowDetails; + private float mUserDistance; // in pixel + + private ProgressDialog mProgressDialog; + private Future<?> mPendingTask; + + private Future<Void> mSyncTask = null; + + private GLView mRootPane = new GLView() { + private float mMatrix[] = new float[16]; + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + mStaticBackground.layout(0, 0, right - left, bottom - top); + + int slotViewTop = GalleryActionBar.getHeight((Activity) mActivity); + int slotViewBottom = bottom - top; + int slotViewRight = right - left; + + if (mShowDetails) { + mDetailsWindow.measure( + MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + int width = mDetailsWindow.getMeasuredWidth(); + int detailLeft = right - left - width; + slotViewRight = detailLeft; + mDetailsWindow.layout(detailLeft, slotViewTop, detailLeft + width, + bottom - top); + } else { + mAlbumView.setSelectionDrawer(mGridDrawer); + } + + mAlbumView.layout(0, slotViewTop, slotViewRight, slotViewBottom); + GalleryUtils.setViewPointMatrix(mMatrix, + (right - left) / 2, (bottom - top) / 2, -mUserDistance); + PositionRepository.getInstance(mActivity).setOffset( + 0, slotViewTop); + } + + @Override + protected void render(GLCanvas canvas) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + canvas.multiplyMatrix(mMatrix, 0); + super.render(canvas); + canvas.restore(); + } + }; + + @Override + protected void onBackPressed() { + if (mShowDetails) { + hideDetails(); + } else if (mSelectionManager.inSelectionMode()) { + mSelectionManager.leaveSelectionMode(); + } else { + mAlbumView.savePositions(PositionRepository.getInstance(mActivity)); + super.onBackPressed(); + } + } + + public void onSingleTapUp(int slotIndex) { + MediaItem item = mAlbumDataAdapter.get(slotIndex); + if (item == null) { + Log.w(TAG, "item not ready yet, ignore the click"); + return; + } + if (mShowDetails) { + mHighlightDrawer.setHighlightItem(item.getPath()); + mDetailsWindow.reloadDetails(slotIndex); + } else if (!mSelectionManager.inSelectionMode()) { + if (mGetContent) { + onGetContent(item); + } else { + boolean playVideo = + (item.getSupportedOperations() & MediaItem.SUPPORT_PLAY) != 0; + if (playVideo) { + // Play the video. + PhotoPage.playVideo((Activity) mActivity, item.getPlayUri(), item.getName()); + } else { + // Get into the PhotoPage. + Bundle data = new Bundle(); + mAlbumView.savePositions(PositionRepository.getInstance(mActivity)); + data.putInt(PhotoPage.KEY_INDEX_HINT, slotIndex); + data.putString(PhotoPage.KEY_MEDIA_SET_PATH, + mMediaSetPath.toString()); + data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, + item.getPath().toString()); + mActivity.getStateManager().startStateForResult( + PhotoPage.class, REQUEST_PHOTO, data); + } + } + } else { + mSelectionManager.toggle(item.getPath()); + mAlbumView.invalidate(); + } + } + + private void onGetContent(final MediaItem item) { + DataManager dm = mActivity.getDataManager(); + Activity activity = (Activity) mActivity; + if (mData.getString(Gallery.EXTRA_CROP) != null) { + // TODO: Handle MtpImagew + Uri uri = dm.getContentUri(item.getPath()); + Intent intent = new Intent(CropImage.ACTION_CROP, uri) + .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT) + .putExtras(getData()); + if (mData.getParcelable(MediaStore.EXTRA_OUTPUT) == null) { + intent.putExtra(CropImage.KEY_RETURN_DATA, true); + } + activity.startActivity(intent); + activity.finish(); + } else { + activity.setResult(Activity.RESULT_OK, + new Intent(null, item.getContentUri())); + activity.finish(); + } + } + + public void onLongTap(int slotIndex) { + if (mGetContent) return; + if (mShowDetails) { + onSingleTapUp(slotIndex); + } else { + MediaItem item = mAlbumDataAdapter.get(slotIndex); + if (item == null) return; + mSelectionManager.setAutoLeaveSelectionMode(true); + mSelectionManager.toggle(item.getPath()); + mAlbumView.invalidate(); + } + } + + public void doCluster(int clusterType) { + String basePath = mMediaSet.getPath().toString(); + String newPath = FilterUtils.newClusterPath(basePath, clusterType); + Bundle data = new Bundle(getData()); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, newPath); + if (mShowClusterMenu) { + Context context = mActivity.getAndroidContext(); + data.putString(AlbumSetPage.KEY_SET_TITLE, mMediaSet.getName()); + data.putString(AlbumSetPage.KEY_SET_SUBTITLE, + GalleryActionBar.getClusterByTypeString(context, clusterType)); + } + + mAlbumView.savePositions(PositionRepository.getInstance(mActivity)); + mActivity.getStateManager().startStateForResult( + AlbumSetPage.class, REQUEST_DO_ANIMATION, data); + } + + public void doFilter(int filterType) { + String basePath = mMediaSet.getPath().toString(); + String newPath = FilterUtils.switchFilterPath(basePath, filterType); + Bundle data = new Bundle(getData()); + data.putString(AlbumPage.KEY_MEDIA_PATH, newPath); + mAlbumView.savePositions(PositionRepository.getInstance(mActivity)); + mActivity.getStateManager().switchState(this, AlbumPage.class, data); + } + + public void onOperationComplete() { + mAlbumView.invalidate(); + // TODO: enable animation + } + + @Override + protected void onCreate(Bundle data, Bundle restoreState) { + mUserDistance = GalleryUtils.meterToPixel(USER_DISTANCE_METER); + initializeViews(); + initializeData(data); + mGetContent = data.getBoolean(Gallery.KEY_GET_CONTENT, false); + mShowClusterMenu = data.getBoolean(KEY_SHOW_CLUSTER_MENU, false); + + startTransition(data); + + // Enable auto-select-all for mtp album + if (data.getBoolean(KEY_AUTO_SELECT_ALL)) { + mSelectionManager.selectAll(); + } + } + + private void startTransition() { + final PositionRepository repository = + PositionRepository.getInstance(mActivity); + mAlbumView.startTransition(new PositionProvider() { + private Position mTempPosition = new Position(); + public Position getPosition(long identity, Position target) { + Position p = repository.get(identity); + if (p != null) return p; + mTempPosition.set(target); + mTempPosition.z = 128; + return mTempPosition; + } + }); + } + + private void startTransition(Bundle data) { + final PositionRepository repository = + PositionRepository.getInstance(mActivity); + final int[] center = data == null + ? null + : data.getIntArray(KEY_SET_CENTER); + final Random random = new Random(); + mAlbumView.startTransition(new PositionProvider() { + private Position mTempPosition = new Position(); + public Position getPosition(long identity, Position target) { + Position p = repository.get(identity); + if (p != null) return p; + if (center != null) { + random.setSeed(identity); + mTempPosition.set(center[0], center[1], + 0, random.nextInt(60) - 30, 0); + } else { + mTempPosition.set(target); + mTempPosition.z = 128; + } + return mTempPosition; + } + }); + } + + @Override + protected void onResume() { + super.onResume(); + mIsActive = true; + setContentPane(mRootPane); + mAlbumDataAdapter.resume(); + mAlbumView.resume(); + mActionModeHandler.resume(); + } + + @Override + protected void onPause() { + super.onPause(); + mIsActive = false; + mAlbumDataAdapter.pause(); + mAlbumView.pause(); + if (mDetailsWindow != null) { + mDetailsWindow.pause(); + } + Future<?> task = mPendingTask; + if (task != null) { + // cancel on going task + task.cancel(); + task.waitDone(); + if (mProgressDialog != null) { + mProgressDialog.dismiss(); + mProgressDialog = null; + } + } + if (mSyncTask != null) { + mSyncTask.cancel(); + mSyncTask = null; + } + mActionModeHandler.pause(); + } + + @Override + protected void onDestroy() { + if (mAlbumDataAdapter != null) { + mAlbumDataAdapter.setLoadingListener(null); + } + } + + private void initializeViews() { + mStaticBackground = new StaticBackground((Context) mActivity); + mRootPane.addComponent(mStaticBackground); + + mSelectionManager = new SelectionManager(mActivity, false); + mSelectionManager.setSelectionListener(this); + mGridDrawer = new GridDrawer((Context) mActivity, mSelectionManager); + Config.AlbumPage config = Config.AlbumPage.get((Context) mActivity); + mAlbumView = new AlbumView(mActivity, + config.slotWidth, config.slotHeight, config.displayItemSize); + mAlbumView.setSelectionDrawer(mGridDrawer); + mRootPane.addComponent(mAlbumView); + mAlbumView.setListener(new SlotView.SimpleListener() { + @Override + public void onSingleTapUp(int slotIndex) { + AlbumPage.this.onSingleTapUp(slotIndex); + } + @Override + public void onLongTap(int slotIndex) { + AlbumPage.this.onLongTap(slotIndex); + } + }); + mActionModeHandler = new ActionModeHandler(mActivity, mSelectionManager); + mActionModeHandler.setActionModeListener(new ActionModeListener() { + public boolean onActionItemClicked(MenuItem item) { + return onItemSelected(item); + } + }); + mStaticBackground.setImage(R.drawable.background, + R.drawable.background_portrait); + } + + private void initializeData(Bundle data) { + mMediaSetPath = Path.fromString(data.getString(KEY_MEDIA_PATH)); + mMediaSet = mActivity.getDataManager().getMediaSet(mMediaSetPath); + Utils.assertTrue(mMediaSet != null, + "MediaSet is null. Path = %s", mMediaSetPath); + mSelectionManager.setSourceMediaSet(mMediaSet); + mAlbumDataAdapter = new AlbumDataAdapter(mActivity, mMediaSet); + mAlbumDataAdapter.setLoadingListener(new MyLoadingListener()); + mAlbumView.setModel(mAlbumDataAdapter); + } + + private void showDetails() { + mShowDetails = true; + if (mDetailsWindow == null) { + mHighlightDrawer = new HighlightDrawer(mActivity.getAndroidContext()); + mDetailsWindow = new DetailsWindow(mActivity, new MyDetailsSource()); + mDetailsWindow.setCloseListener(new CloseListener() { + public void onClose() { + hideDetails(); + } + }); + mRootPane.addComponent(mDetailsWindow); + } + mAlbumView.setSelectionDrawer(mHighlightDrawer); + mDetailsWindow.show(); + } + + private void hideDetails() { + mShowDetails = false; + mAlbumView.setSelectionDrawer(mGridDrawer); + mDetailsWindow.hide(); + } + + @Override + protected boolean onCreateActionBar(Menu menu) { + Activity activity = (Activity) mActivity; + GalleryActionBar actionBar = mActivity.getGalleryActionBar(); + MenuInflater inflater = activity.getMenuInflater(); + + if (mGetContent) { + inflater.inflate(R.menu.pickup, menu); + int typeBits = mData.getInt(Gallery.KEY_TYPE_BITS, + DataManager.INCLUDE_IMAGE); + + actionBar.setTitle(GalleryUtils.getSelectionModePrompt(typeBits)); + } else { + inflater.inflate(R.menu.album, menu); + actionBar.setTitle(mMediaSet.getName()); + if (mMediaSet instanceof MtpDevice) { + menu.findItem(R.id.action_slideshow).setVisible(false); + } else { + menu.findItem(R.id.action_slideshow).setVisible(true); + } + + MenuItem groupBy = menu.findItem(R.id.action_group_by); + FilterUtils.setupMenuItems(actionBar, mMediaSetPath, true); + + if (groupBy != null) { + groupBy.setVisible(mShowClusterMenu); + } + + actionBar.setTitle(mMediaSet.getName()); + } + actionBar.setSubtitle(null); + + return true; + } + + @Override + protected boolean onItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_select: + mSelectionManager.setAutoLeaveSelectionMode(false); + mSelectionManager.enterSelectionMode(); + return true; + case R.id.action_group_by: { + mActivity.getGalleryActionBar().showClusterDialog(this); + return true; + } + case R.id.action_slideshow: { + Bundle data = new Bundle(); + data.putString(SlideshowPage.KEY_SET_PATH, + mMediaSetPath.toString()); + data.putBoolean(SlideshowPage.KEY_REPEAT, true); + mActivity.getStateManager().startStateForResult( + SlideshowPage.class, REQUEST_SLIDESHOW, data); + return true; + } + case R.id.action_details: { + if (mShowDetails) { + hideDetails(); + } else { + showDetails(); + } + return true; + } + default: + return false; + } + } + + @Override + protected void onStateResult(int request, int result, Intent data) { + switch (request) { + case REQUEST_SLIDESHOW: { + // data could be null, if there is no images in the album + if (data == null) return; + mFocusIndex = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0); + mAlbumView.setCenterIndex(mFocusIndex); + break; + } + case REQUEST_PHOTO: { + if (data == null) return; + mFocusIndex = data.getIntExtra(PhotoPage.KEY_INDEX_HINT, 0); + mAlbumView.setCenterIndex(mFocusIndex); + startTransition(); + break; + } + case REQUEST_DO_ANIMATION: { + startTransition(null); + break; + } + } + } + + public void onSelectionModeChange(int mode) { + switch (mode) { + case SelectionManager.ENTER_SELECTION_MODE: { + mActionMode = mActionModeHandler.startActionMode(); + break; + } + case SelectionManager.LEAVE_SELECTION_MODE: { + mActionMode.finish(); + mRootPane.invalidate(); + break; + } + case SelectionManager.SELECT_ALL_MODE: { + int count = mSelectionManager.getSelectedCount(); + String format = mActivity.getResources().getQuantityString( + R.plurals.number_of_items_selected, count); + mActionModeHandler.setTitle(String.format(format, count)); + mActionModeHandler.updateSupportedOperation(); + mRootPane.invalidate(); + break; + } + } + } + + public void onSelectionChange(Path path, boolean selected) { + Utils.assertTrue(mActionMode != null); + int count = mSelectionManager.getSelectedCount(); + String format = mActivity.getResources().getQuantityString( + R.plurals.number_of_items_selected, count); + mActionModeHandler.setTitle(String.format(format, count)); + mActionModeHandler.updateSupportedOperation(path, selected); + } + + private class MyLoadingListener implements LoadingListener { + @Override + public void onLoadingStarted() { + GalleryUtils.setSpinnerVisibility((Activity) mActivity, true); + } + + @Override + public void onLoadingFinished() { + if (!mIsActive) return; + if (mAlbumDataAdapter.size() == 0) { + if (mSyncTask == null) { + mSyncTask = mMediaSet.requestSync(); + } + if (mSyncTask.isDone()){ + Toast.makeText((Context) mActivity, + R.string.empty_album, Toast.LENGTH_LONG).show(); + mActivity.getStateManager().finishState(AlbumPage.this); + } + } + if (mSyncTask == null || mSyncTask.isDone()) { + GalleryUtils.setSpinnerVisibility((Activity) mActivity, false); + } + } + } + + private class MyDetailsSource implements DetailsWindow.DetailsSource { + private int mIndex; + public int size() { + return mAlbumDataAdapter.size(); + } + + // If requested index is out of active window, suggest a valid index. + // If there is no valid index available, return -1. + public int findIndex(int indexHint) { + if (mAlbumDataAdapter.isActive(indexHint)) { + mIndex = indexHint; + } else { + mIndex = mAlbumDataAdapter.getActiveStart(); + if (!mAlbumDataAdapter.isActive(mIndex)) { + return -1; + } + } + return mIndex; + } + + public MediaDetails getDetails() { + MediaObject item = mAlbumDataAdapter.get(mIndex); + if (item != null) { + mHighlightDrawer.setHighlightItem(item.getPath()); + return item.getDetails(); + } else { + return null; + } + } + } +} diff --git a/src/com/android/gallery3d/app/AlbumPicker.java b/src/com/android/gallery3d/app/AlbumPicker.java new file mode 100644 index 000000000..b86aee879 --- /dev/null +++ b/src/com/android/gallery3d/app/AlbumPicker.java @@ -0,0 +1,68 @@ +/* + * 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.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.ui.GLRoot; +import com.android.gallery3d.ui.GLRootView; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; + +public class AlbumPicker extends AbstractGalleryActivity + implements OnClickListener { + + public static final String KEY_ALBUM_PATH = "album-path"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.dialog_picker); + ((GLRootView) findViewById(R.id.gl_root_view)).setZOrderOnTop(true); + findViewById(R.id.cancel).setOnClickListener(this); + setTitle(R.string.select_album); + Intent intent = getIntent(); + Bundle extras = intent.getExtras(); + Bundle data = extras == null ? new Bundle() : new Bundle(extras); + + data.putBoolean(Gallery.KEY_GET_ALBUM, true); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, + getDataManager().getTopSetPath(DataManager.INCLUDE_IMAGE)); + getStateManager().startState(AlbumSetPage.class, data); + } + + @Override + public void onBackPressed() { + // send the back event to the top sub-state + GLRoot root = getGLRoot(); + root.lockRenderThread(); + try { + getStateManager().getTopState().onBackPressed(); + } finally { + root.unlockRenderThread(); + } + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.cancel) finish(); + } +} diff --git a/src/com/android/gallery3d/app/AlbumSetDataAdapter.java b/src/com/android/gallery3d/app/AlbumSetDataAdapter.java new file mode 100644 index 000000000..9086ddbf4 --- /dev/null +++ b/src/com/android/gallery3d/app/AlbumSetDataAdapter.java @@ -0,0 +1,384 @@ +/* + * 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.app; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.ui.AlbumSetView; +import com.android.gallery3d.ui.SynchronizedHandler; + +import android.os.Handler; +import android.os.Message; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +public class AlbumSetDataAdapter implements AlbumSetView.Model { + @SuppressWarnings("unused") + private static final String TAG = "AlbumSetDataAdapter"; + + private static final int INDEX_NONE = -1; + + private static final int MIN_LOAD_COUNT = 4; + private static final int MAX_COVER_COUNT = 4; + + private static final int MSG_LOAD_START = 1; + private static final int MSG_LOAD_FINISH = 2; + private static final int MSG_RUN_OBJECT = 3; + + private static final MediaItem[] EMPTY_MEDIA_ITEMS = new MediaItem[0]; + + private final MediaSet[] mData; + private final MediaItem[][] mCoverData; + private final long[] mItemVersion; + private final long[] mSetVersion; + + private int mActiveStart = 0; + private int mActiveEnd = 0; + + private int mContentStart = 0; + private int mContentEnd = 0; + + private final MediaSet mSource; + private long mSourceVersion = MediaObject.INVALID_DATA_VERSION; + private int mSize; + + private AlbumSetView.ModelListener mModelListener; + private LoadingListener mLoadingListener; + private ReloadTask mReloadTask; + + private final Handler mMainHandler; + + private MySourceListener mSourceListener = new MySourceListener(); + + public AlbumSetDataAdapter(GalleryActivity activity, MediaSet albumSet, int cacheSize) { + mSource = Utils.checkNotNull(albumSet); + mCoverData = new MediaItem[cacheSize][]; + mData = new MediaSet[cacheSize]; + mItemVersion = new long[cacheSize]; + mSetVersion = new long[cacheSize]; + Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION); + Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION); + + mMainHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_RUN_OBJECT: + ((Runnable) message.obj).run(); + return; + case MSG_LOAD_START: + if (mLoadingListener != null) mLoadingListener.onLoadingStarted(); + return; + case MSG_LOAD_FINISH: + if (mLoadingListener != null) mLoadingListener.onLoadingFinished(); + return; + } + } + }; + } + + public void pause() { + mReloadTask.terminate(); + mReloadTask = null; + mSource.removeContentListener(mSourceListener); + } + + public void resume() { + mSource.addContentListener(mSourceListener); + mReloadTask = new ReloadTask(); + mReloadTask.start(); + } + + public MediaSet getMediaSet(int index) { + if (index < mActiveStart && index >= mActiveEnd) { + throw new IllegalArgumentException(String.format( + "%s not in (%s, %s)", index, mActiveStart, mActiveEnd)); + } + return mData[index % mData.length]; + } + + public MediaItem[] getCoverItems(int index) { + if (index < mActiveStart && index >= mActiveEnd) { + throw new IllegalArgumentException(String.format( + "%s not in (%s, %s)", index, mActiveStart, mActiveEnd)); + } + MediaItem[] result = mCoverData[index % mCoverData.length]; + + // If the result is not ready yet, return an empty array + return result == null ? EMPTY_MEDIA_ITEMS : result; + } + + public int getActiveStart() { + return mActiveStart; + } + + public int getActiveEnd() { + return mActiveEnd; + } + + public boolean isActive(int index) { + return index >= mActiveStart && index < mActiveEnd; + } + + public int size() { + return mSize; + } + + private void clearSlot(int slotIndex) { + mData[slotIndex] = null; + mCoverData[slotIndex] = null; + mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION; + mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION; + } + + private void setContentWindow(int contentStart, int contentEnd) { + if (contentStart == mContentStart && contentEnd == mContentEnd) return; + MediaItem[][] data = mCoverData; + int length = data.length; + + int start = this.mContentStart; + int end = this.mContentEnd; + + mContentStart = contentStart; + mContentEnd = contentEnd; + + if (contentStart >= end || start >= contentEnd) { + for (int i = start, n = end; i < n; ++i) { + clearSlot(i % length); + } + } else { + for (int i = start; i < contentStart; ++i) { + clearSlot(i % length); + } + for (int i = contentEnd, n = end; i < n; ++i) { + clearSlot(i % length); + } + } + mReloadTask.notifyDirty(); + } + + public void setActiveWindow(int start, int end) { + if (start == mActiveStart && end == mActiveEnd) return; + + Utils.assertTrue(start <= end + && end - start <= mCoverData.length && end <= mSize); + + mActiveStart = start; + mActiveEnd = end; + + int length = mCoverData.length; + // If no data is visible, keep the cache content + if (start == end) return; + + int contentStart = Utils.clamp((start + end) / 2 - length / 2, + 0, Math.max(0, mSize - length)); + int contentEnd = Math.min(contentStart + length, mSize); + if (mContentStart > start || mContentEnd < end + || Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) { + setContentWindow(contentStart, contentEnd); + } + } + + private class MySourceListener implements ContentListener { + public void onContentDirty() { + mReloadTask.notifyDirty(); + } + } + + public void setModelListener(AlbumSetView.ModelListener listener) { + mModelListener = listener; + } + + public void setLoadingListener(LoadingListener listener) { + mLoadingListener = listener; + } + + private static void getRepresentativeItems(MediaSet set, int wanted, + ArrayList<MediaItem> result) { + if (set.getMediaItemCount() > 0) { + result.addAll(set.getMediaItem(0, wanted)); + } + + int n = set.getSubMediaSetCount(); + for (int i = 0; i < n && wanted > result.size(); i++) { + MediaSet subset = set.getSubMediaSet(i); + double perSet = (double) (wanted - result.size()) / (n - i); + int m = (int) Math.ceil(perSet); + getRepresentativeItems(subset, m, result); + } + } + + private static class UpdateInfo { + public long version; + public int index; + + public int size; + public MediaSet item; + public MediaItem covers[]; + } + + private class GetUpdateInfo implements Callable<UpdateInfo> { + + private final long mVersion; + + public GetUpdateInfo(long version) { + mVersion = version; + } + + private int getInvalidIndex(long version) { + long setVersion[] = mSetVersion; + int length = setVersion.length; + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + int index = i % length; + if (setVersion[i % length] != version) return i; + } + return INDEX_NONE; + } + + @Override + public UpdateInfo call() throws Exception { + int index = getInvalidIndex(mVersion); + if (index == INDEX_NONE + && mSourceVersion == mVersion) return null; + UpdateInfo info = new UpdateInfo(); + info.version = mSourceVersion; + info.index = index; + info.size = mSize; + return info; + } + } + + private class UpdateContent implements Callable<Void> { + private UpdateInfo mUpdateInfo; + + public UpdateContent(UpdateInfo info) { + mUpdateInfo = info; + } + + public Void call() { + UpdateInfo info = mUpdateInfo; + mSourceVersion = info.version; + if (mSize != info.size) { + mSize = info.size; + if (mModelListener != null) mModelListener.onSizeChanged(mSize); + if (mContentEnd > mSize) mContentEnd = mSize; + if (mActiveEnd > mSize) mActiveEnd = mSize; + } + // Note: info.index could be INDEX_NONE, i.e., -1 + if (info.index >= mContentStart && info.index < mContentEnd) { + int pos = info.index % mCoverData.length; + mSetVersion[pos] = info.version; + long itemVersion = info.item.getDataVersion(); + if (mItemVersion[pos] == itemVersion) return null; + mItemVersion[pos] = itemVersion; + mData[pos] = info.item; + mCoverData[pos] = info.covers; + if (mModelListener != null + && info.index >= mActiveStart && info.index < mActiveEnd) { + mModelListener.onWindowContentChanged(info.index); + } + } + return null; + } + } + + private <T> T executeAndWait(Callable<T> callable) { + FutureTask<T> task = new FutureTask<T>(callable); + mMainHandler.sendMessage( + mMainHandler.obtainMessage(MSG_RUN_OBJECT, task)); + try { + return task.get(); + } catch (InterruptedException e) { + return null; + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + // TODO: load active range first + private class ReloadTask extends Thread { + private volatile boolean mActive = true; + private volatile boolean mDirty = true; + private volatile boolean mIsLoading = false; + + private void updateLoading(boolean loading) { + if (mIsLoading == loading) return; + mIsLoading = loading; + mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH); + } + + @Override + public void run() { + boolean updateComplete = false; + while (mActive) { + synchronized (this) { + if (mActive && !mDirty && updateComplete) { + updateLoading(false); + Utils.waitWithoutInterrupt(this); + continue; + } + } + mDirty = false; + updateLoading(true); + + long version; + synchronized (DataManager.LOCK) { + version = mSource.reload(); + } + UpdateInfo info = executeAndWait(new GetUpdateInfo(version)); + updateComplete = info == null; + if (updateComplete) continue; + + synchronized (DataManager.LOCK) { + if (info.version != version) { + info.version = version; + info.size = mSource.getSubMediaSetCount(); + } + if (info.index != INDEX_NONE) { + info.item = mSource.getSubMediaSet(info.index); + if (info.item == null) continue; + ArrayList<MediaItem> covers = new ArrayList<MediaItem>(); + getRepresentativeItems(info.item, MAX_COVER_COUNT, covers); + info.covers = covers.toArray(new MediaItem[covers.size()]); + } + } + executeAndWait(new UpdateContent(info)); + } + updateLoading(false); + } + + public synchronized void notifyDirty() { + mDirty = true; + notifyAll(); + } + + public synchronized void terminate() { + mActive = false; + notifyAll(); + } + } +} + + diff --git a/src/com/android/gallery3d/app/AlbumSetPage.java b/src/com/android/gallery3d/app/AlbumSetPage.java new file mode 100644 index 000000000..688ff81f2 --- /dev/null +++ b/src/com/android/gallery3d/app/AlbumSetPage.java @@ -0,0 +1,586 @@ +/* + * 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.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaDetails; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.picasasource.PicasaSource; +import com.android.gallery3d.settings.GallerySettings; +import com.android.gallery3d.ui.ActionModeHandler; +import com.android.gallery3d.ui.ActionModeHandler.ActionModeListener; +import com.android.gallery3d.ui.AlbumSetView; +import com.android.gallery3d.ui.DetailsWindow; +import com.android.gallery3d.ui.DetailsWindow.CloseListener; +import com.android.gallery3d.ui.GLCanvas; +import com.android.gallery3d.ui.GLView; +import com.android.gallery3d.ui.GridDrawer; +import com.android.gallery3d.ui.HighlightDrawer; +import com.android.gallery3d.ui.PositionProvider; +import com.android.gallery3d.ui.PositionRepository; +import com.android.gallery3d.ui.PositionRepository.Position; +import com.android.gallery3d.ui.SelectionManager; +import com.android.gallery3d.ui.SlotView; +import com.android.gallery3d.ui.StaticBackground; +import com.android.gallery3d.ui.SynchronizedHandler; +import com.android.gallery3d.util.GalleryUtils; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Message; +import android.provider.MediaStore; +import android.view.ActionMode; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View.MeasureSpec; +import android.widget.Toast; + +public class AlbumSetPage extends ActivityState implements + SelectionManager.SelectionListener, GalleryActionBar.ClusterRunner, + EyePosition.EyePositionListener { + @SuppressWarnings("unused") + private static final String TAG = "AlbumSetPage"; + + public static final String KEY_MEDIA_PATH = "media-path"; + public static final String KEY_SET_TITLE = "set-title"; + public static final String KEY_SET_SUBTITLE = "set-subtitle"; + private static final int DATA_CACHE_SIZE = 256; + private static final int REQUEST_DO_ANIMATION = 1; + private static final int MSG_GOTO_MANAGE_CACHE_PAGE = 1; + + private boolean mIsActive = false; + private StaticBackground mStaticBackground; + private AlbumSetView mAlbumSetView; + + private MediaSet mMediaSet; + private String mTitle; + private String mSubtitle; + private boolean mShowClusterTabs; + + protected SelectionManager mSelectionManager; + private AlbumSetDataAdapter mAlbumSetDataAdapter; + private GridDrawer mGridDrawer; + private HighlightDrawer mHighlightDrawer; + + private boolean mGetContent; + private boolean mGetAlbum; + private ActionMode mActionMode; + private ActionModeHandler mActionModeHandler; + private DetailsWindow mDetailsWindow; + private boolean mShowDetails; + private EyePosition mEyePosition; + + // The eyes' position of the user, the origin is at the center of the + // device and the unit is in pixels. + private float mX; + private float mY; + private float mZ; + + private SynchronizedHandler mHandler; + + private GLView mRootPane = new GLView() { + private float mMatrix[] = new float[16]; + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + mStaticBackground.layout(0, 0, right - left, bottom - top); + mEyePosition.resetPosition(); + + int slotViewTop = GalleryActionBar.getHeight((Activity) mActivity); + int slotViewBottom = bottom - top; + int slotViewRight = right - left; + + if (mShowDetails) { + mDetailsWindow.measure( + MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + int width = mDetailsWindow.getMeasuredWidth(); + int detailLeft = right - left - width; + slotViewRight = detailLeft; + mDetailsWindow.layout(detailLeft, slotViewTop, detailLeft + width, + bottom - top); + } else { + mAlbumSetView.setSelectionDrawer(mGridDrawer); + } + + mAlbumSetView.layout(0, slotViewTop, slotViewRight, slotViewBottom); + PositionRepository.getInstance(mActivity).setOffset( + 0, slotViewTop); + } + + @Override + protected void render(GLCanvas canvas) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + GalleryUtils.setViewPointMatrix(mMatrix, + getWidth() / 2 + mX, getHeight() / 2 + mY, mZ); + canvas.multiplyMatrix(mMatrix, 0); + super.render(canvas); + canvas.restore(); + } + }; + + @Override + public void onEyePositionChanged(float x, float y, float z) { + mRootPane.lockRendering(); + mX = x; + mY = y; + mZ = z; + mRootPane.unlockRendering(); + mRootPane.invalidate(); + } + + @Override + public void onBackPressed() { + if (mShowDetails) { + hideDetails(); + } else if (mSelectionManager.inSelectionMode()) { + mSelectionManager.leaveSelectionMode(); + } else { + mAlbumSetView.savePositions( + PositionRepository.getInstance(mActivity)); + super.onBackPressed(); + } + } + + private void savePositions(int slotIndex, int center[]) { + Rect offset = new Rect(); + mRootPane.getBoundsOf(mAlbumSetView, offset); + mAlbumSetView.savePositions(PositionRepository.getInstance(mActivity)); + Rect r = mAlbumSetView.getSlotRect(slotIndex); + int scrollX = mAlbumSetView.getScrollX(); + int scrollY = mAlbumSetView.getScrollY(); + center[0] = offset.left + (r.left + r.right) / 2 - scrollX; + center[1] = offset.top + (r.top + r.bottom) / 2 - scrollY; + } + + public void onSingleTapUp(int slotIndex) { + MediaSet targetSet = mAlbumSetDataAdapter.getMediaSet(slotIndex); + if (targetSet == null) return; // Content is dirty, we shall reload soon + + if (mShowDetails) { + Path path = targetSet.getPath(); + mHighlightDrawer.setHighlightItem(path); + mDetailsWindow.reloadDetails(slotIndex); + } else if (!mSelectionManager.inSelectionMode()) { + Bundle data = new Bundle(getData()); + String mediaPath = targetSet.getPath().toString(); + int[] center = new int[2]; + savePositions(slotIndex, center); + data.putIntArray(AlbumPage.KEY_SET_CENTER, center); + if (mGetAlbum && targetSet.isLeafAlbum()) { + Activity activity = (Activity) mActivity; + Intent result = new Intent() + .putExtra(AlbumPicker.KEY_ALBUM_PATH, targetSet.getPath().toString()); + activity.setResult(Activity.RESULT_OK, result); + activity.finish(); + } else if (targetSet.getSubMediaSetCount() > 0) { + data.putString(AlbumSetPage.KEY_MEDIA_PATH, mediaPath); + mActivity.getStateManager().startStateForResult( + AlbumSetPage.class, REQUEST_DO_ANIMATION, data); + } else { + if (!mGetContent && (targetSet.getSupportedOperations() + & MediaObject.SUPPORT_IMPORT) != 0) { + data.putBoolean(AlbumPage.KEY_AUTO_SELECT_ALL, true); + } + data.putString(AlbumPage.KEY_MEDIA_PATH, mediaPath); + boolean inAlbum = mActivity.getStateManager().hasStateClass(AlbumPage.class); + // We only show cluster menu in the first AlbumPage in stack + data.putBoolean(AlbumPage.KEY_SHOW_CLUSTER_MENU, !inAlbum); + mActivity.getStateManager().startStateForResult( + AlbumPage.class, REQUEST_DO_ANIMATION, data); + } + } else { + mSelectionManager.toggle(targetSet.getPath()); + mAlbumSetView.invalidate(); + } + } + + public void onLongTap(int slotIndex) { + if (mGetContent || mGetAlbum) return; + if (mShowDetails) { + onSingleTapUp(slotIndex); + } else { + MediaSet set = mAlbumSetDataAdapter.getMediaSet(slotIndex); + if (set == null) return; + mSelectionManager.setAutoLeaveSelectionMode(true); + mSelectionManager.toggle(set.getPath()); + mAlbumSetView.invalidate(); + } + } + + public void doCluster(int clusterType) { + String basePath = mMediaSet.getPath().toString(); + String newPath = FilterUtils.switchClusterPath(basePath, clusterType); + Bundle data = new Bundle(getData()); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, newPath); + mAlbumSetView.savePositions(PositionRepository.getInstance(mActivity)); + mActivity.getStateManager().switchState(this, AlbumSetPage.class, data); + } + + public void doFilter(int filterType) { + String basePath = mMediaSet.getPath().toString(); + String newPath = FilterUtils.switchFilterPath(basePath, filterType); + Bundle data = new Bundle(getData()); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, newPath); + mAlbumSetView.savePositions(PositionRepository.getInstance(mActivity)); + mActivity.getStateManager().switchState(this, AlbumSetPage.class, data); + } + + public void onOperationComplete() { + mAlbumSetView.invalidate(); + // TODO: enable animation + } + + @Override + public void onCreate(Bundle data, Bundle restoreState) { + mHandler = new SynchronizedHandler(mActivity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + Utils.assertTrue(message.what == MSG_GOTO_MANAGE_CACHE_PAGE); + Bundle data = new Bundle(); + String mediaPath = mActivity.getDataManager().getTopSetPath( + DataManager.INCLUDE_ALL); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, mediaPath); + mActivity.getStateManager().startState(ManageCachePage.class, data); + } + }; + + initializeViews(); + initializeData(data); + mGetContent = data.getBoolean(Gallery.KEY_GET_CONTENT, false); + mGetAlbum = data.getBoolean(Gallery.KEY_GET_ALBUM, false); + mTitle = data.getString(AlbumSetPage.KEY_SET_TITLE); + mSubtitle = data.getString(AlbumSetPage.KEY_SET_SUBTITLE); + mEyePosition = new EyePosition(mActivity.getAndroidContext(), this); + + startTransition(); + } + + @Override + public void onPause() { + super.onPause(); + mIsActive = false; + mActionModeHandler.pause(); + mAlbumSetDataAdapter.pause(); + mAlbumSetView.pause(); + mEyePosition.pause(); + if (mDetailsWindow != null) { + mDetailsWindow.pause(); + } + GalleryActionBar actionBar = mActivity.getGalleryActionBar(); + if (actionBar != null) actionBar.hideClusterTabs(); + } + + @Override + public void onResume() { + super.onResume(); + mIsActive = true; + setContentPane(mRootPane); + mAlbumSetDataAdapter.resume(); + mAlbumSetView.resume(); + mEyePosition.resume(); + mActionModeHandler.resume(); + GalleryActionBar actionBar = mActivity.getGalleryActionBar(); + if (mShowClusterTabs && actionBar != null) actionBar.showClusterTabs(this); + } + + private void initializeData(Bundle data) { + String mediaPath = data.getString(AlbumSetPage.KEY_MEDIA_PATH); + mMediaSet = mActivity.getDataManager().getMediaSet(mediaPath); + mSelectionManager.setSourceMediaSet(mMediaSet); + mAlbumSetDataAdapter = new AlbumSetDataAdapter( + mActivity, mMediaSet, DATA_CACHE_SIZE); + mAlbumSetDataAdapter.setLoadingListener(new MyLoadingListener()); + mAlbumSetView.setModel(mAlbumSetDataAdapter); + } + + private void initializeViews() { + mSelectionManager = new SelectionManager(mActivity, true); + mSelectionManager.setSelectionListener(this); + mStaticBackground = new StaticBackground(mActivity.getAndroidContext()); + mRootPane.addComponent(mStaticBackground); + + mGridDrawer = new GridDrawer((Context) mActivity, mSelectionManager); + Config.AlbumSetPage config = Config.AlbumSetPage.get((Context) mActivity); + mAlbumSetView = new AlbumSetView(mActivity, mGridDrawer, + config.slotWidth, config.slotHeight, + config.displayItemSize, config.labelFontSize, + config.labelOffsetY, config.labelMargin); + mAlbumSetView.setListener(new SlotView.SimpleListener() { + @Override + public void onSingleTapUp(int slotIndex) { + AlbumSetPage.this.onSingleTapUp(slotIndex); + } + @Override + public void onLongTap(int slotIndex) { + AlbumSetPage.this.onLongTap(slotIndex); + } + }); + + mActionModeHandler = new ActionModeHandler(mActivity, mSelectionManager); + mActionModeHandler.setActionModeListener(new ActionModeListener() { + public boolean onActionItemClicked(MenuItem item) { + return onItemSelected(item); + } + }); + mRootPane.addComponent(mAlbumSetView); + + mStaticBackground.setImage(R.drawable.background, + R.drawable.background_portrait); + } + + @Override + protected boolean onCreateActionBar(Menu menu) { + Activity activity = (Activity) mActivity; + GalleryActionBar actionBar = mActivity.getGalleryActionBar(); + MenuInflater inflater = activity.getMenuInflater(); + + final boolean inAlbum = mActivity.getStateManager().hasStateClass( + AlbumPage.class); + + if (mGetContent) { + inflater.inflate(R.menu.pickup, menu); + int typeBits = mData.getInt( + Gallery.KEY_TYPE_BITS, DataManager.INCLUDE_IMAGE); + int id = R.string.select_image; + if ((typeBits & DataManager.INCLUDE_VIDEO) != 0) { + id = (typeBits & DataManager.INCLUDE_IMAGE) == 0 + ? R.string.select_video + : R.string.select_item; + } + actionBar.setTitle(id); + } else if (mGetAlbum) { + inflater.inflate(R.menu.pickup, menu); + actionBar.setTitle(R.string.select_album); + } else { + mShowClusterTabs = !inAlbum; + inflater.inflate(R.menu.albumset, menu); + if (mTitle != null) { + actionBar.setTitle(mTitle); + } else { + actionBar.setTitle(activity.getApplicationInfo().labelRes); + } + MenuItem selectItem = menu.findItem(R.id.action_select); + + if (selectItem != null) { + boolean selectAlbums = !inAlbum && + actionBar.getClusterTypeAction() == FilterUtils.CLUSTER_BY_ALBUM; + if (selectAlbums) { + selectItem.setTitle(R.string.select_album); + } else { + selectItem.setTitle(R.string.select_group); + } + } + + MenuItem switchCamera = menu.findItem(R.id.action_camera); + if (switchCamera != null) { + switchCamera.setVisible(GalleryUtils.isCameraAvailable(activity)); + } + + actionBar.setSubtitle(mSubtitle); + } + return true; + } + + @Override + protected boolean onItemSelected(MenuItem item) { + Activity activity = (Activity) mActivity; + switch (item.getItemId()) { + case R.id.action_select: + mSelectionManager.setAutoLeaveSelectionMode(false); + mSelectionManager.enterSelectionMode(); + return true; + case R.id.action_details: + if (mAlbumSetDataAdapter.size() != 0) { + if (mShowDetails) { + hideDetails(); + } else { + showDetails(); + } + } else { + Toast.makeText(activity, + activity.getText(R.string.no_albums_alert), + Toast.LENGTH_SHORT).show(); + } + return true; + case R.id.action_camera: { + Intent intent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA) + .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP + | Intent.FLAG_ACTIVITY_NEW_TASK); + activity.startActivity(intent); + return true; + } + case R.id.action_manage_offline: { + mHandler.sendEmptyMessage(MSG_GOTO_MANAGE_CACHE_PAGE); + return true; + } + case R.id.action_sync_picasa_albums: { + PicasaSource.requestSync(activity); + return true; + } + case R.id.action_settings: { + activity.startActivity(new Intent(activity, GallerySettings.class)); + return true; + } + default: + return false; + } + } + + @Override + protected void onStateResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_DO_ANIMATION: { + startTransition(); + } + } + } + + private void startTransition() { + final PositionRepository repository = + PositionRepository.getInstance(mActivity); + mAlbumSetView.startTransition(new PositionProvider() { + private Position mTempPosition = new Position(); + public Position getPosition(long identity, Position target) { + Position p = repository.get(identity); + if (p == null) { + p = mTempPosition; + p.set(target.x, target.y, 128, target.theta, 1); + } + return p; + } + }); + } + + private String getSelectedString() { + GalleryActionBar actionBar = mActivity.getGalleryActionBar(); + int count = mSelectionManager.getSelectedCount(); + int action = actionBar.getClusterTypeAction(); + int string = action == FilterUtils.CLUSTER_BY_ALBUM + ? R.plurals.number_of_albums_selected + : R.plurals.number_of_groups_selected; + String format = mActivity.getResources().getQuantityString(string, count); + return String.format(format, count); + } + + public void onSelectionModeChange(int mode) { + + switch (mode) { + case SelectionManager.ENTER_SELECTION_MODE: { + mActivity.getGalleryActionBar().hideClusterTabs(); + mActionMode = mActionModeHandler.startActionMode(); + break; + } + case SelectionManager.LEAVE_SELECTION_MODE: { + mActionMode.finish(); + mActivity.getGalleryActionBar().showClusterTabs(this); + mRootPane.invalidate(); + break; + } + case SelectionManager.SELECT_ALL_MODE: { + mActionModeHandler.setTitle(getSelectedString()); + mRootPane.invalidate(); + break; + } + } + } + + public void onSelectionChange(Path path, boolean selected) { + Utils.assertTrue(mActionMode != null); + mActionModeHandler.setTitle(getSelectedString()); + mActionModeHandler.updateSupportedOperation(path, selected); + } + + private void hideDetails() { + mShowDetails = false; + mAlbumSetView.setSelectionDrawer(mGridDrawer); + mDetailsWindow.hide(); + } + + private void showDetails() { + mShowDetails = true; + if (mDetailsWindow == null) { + mHighlightDrawer = new HighlightDrawer(mActivity.getAndroidContext()); + mDetailsWindow = new DetailsWindow(mActivity, new MyDetailsSource()); + mDetailsWindow.setCloseListener(new CloseListener() { + public void onClose() { + hideDetails(); + } + }); + mRootPane.addComponent(mDetailsWindow); + } + mAlbumSetView.setSelectionDrawer(mHighlightDrawer); + mDetailsWindow.show(); + } + + private class MyLoadingListener implements LoadingListener { + public void onLoadingStarted() { + GalleryUtils.setSpinnerVisibility((Activity) mActivity, true); + } + + public void onLoadingFinished() { + if (!mIsActive) return; + GalleryUtils.setSpinnerVisibility((Activity) mActivity, false); + if (mAlbumSetDataAdapter.size() == 0) { + Toast.makeText((Context) mActivity, + R.string.empty_album, Toast.LENGTH_LONG).show(); + if (mActivity.getStateManager().getStateCount() > 1) { + mActivity.getStateManager().finishState(AlbumSetPage.this); + } + } + } + } + + private class MyDetailsSource implements DetailsWindow.DetailsSource { + private int mIndex; + public int size() { + return mAlbumSetDataAdapter.size(); + } + + // If requested index is out of active window, suggest a valid index. + // If there is no valid index available, return -1. + public int findIndex(int indexHint) { + if (mAlbumSetDataAdapter.isActive(indexHint)) { + mIndex = indexHint; + } else { + mIndex = mAlbumSetDataAdapter.getActiveStart(); + if (!mAlbumSetDataAdapter.isActive(mIndex)) { + return -1; + } + } + return mIndex; + } + + public MediaDetails getDetails() { + MediaObject item = mAlbumSetDataAdapter.getMediaSet(mIndex); + if (item != null) { + mHighlightDrawer.setHighlightItem(item.getPath()); + return item.getDetails(); + } else { + return null; + } + } + } +} diff --git a/src/com/android/gallery3d/app/Config.java b/src/com/android/gallery3d/app/Config.java new file mode 100644 index 000000000..4586235f6 --- /dev/null +++ b/src/com/android/gallery3d/app/Config.java @@ -0,0 +1,140 @@ +/* + * 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.app; + +import com.android.gallery3d.R; + +import android.content.Context; +import android.content.res.Resources; + +final class Config { + public static class AlbumSetPage { + private static AlbumSetPage sInstance; + + public final int slotWidth; + public final int slotHeight; + public final int displayItemSize; + public final int labelFontSize; + public final int labelOffsetY; + public final int labelMargin; + + public static synchronized AlbumSetPage get(Context context) { + if (sInstance == null) { + sInstance = new AlbumSetPage(context); + } + return sInstance; + } + + private AlbumSetPage(Context context) { + Resources r = context.getResources(); + slotWidth = r.getDimensionPixelSize(R.dimen.albumset_slot_width); + slotHeight = r.getDimensionPixelSize(R.dimen.albumset_slot_height); + displayItemSize = r.getDimensionPixelSize(R.dimen.albumset_display_item_size); + labelFontSize = r.getDimensionPixelSize(R.dimen.albumset_label_font_size); + labelOffsetY = r.getDimensionPixelSize(R.dimen.albumset_label_offset_y); + labelMargin = r.getDimensionPixelSize(R.dimen.albumset_label_margin); + } + } + + public static class AlbumPage { + private static AlbumPage sInstance; + + public final int slotWidth; + public final int slotHeight; + public final int displayItemSize; + + public static synchronized AlbumPage get(Context context) { + if (sInstance == null) { + sInstance = new AlbumPage(context); + } + return sInstance; + } + + private AlbumPage(Context context) { + Resources r = context.getResources(); + slotWidth = r.getDimensionPixelSize(R.dimen.album_slot_width); + slotHeight = r.getDimensionPixelSize(R.dimen.album_slot_height); + displayItemSize = r.getDimensionPixelSize(R.dimen.album_display_item_size); + } + } + + public static class ManageCachePage extends AlbumSetPage { + private static ManageCachePage sInstance; + + public final int cacheBarHeight; + public final int cacheBarPinLeftMargin; + public final int cacheBarPinRightMargin; + public final int cacheBarButtonRightMargin; + public final int cacheBarFontSize; + + public static synchronized ManageCachePage get(Context context) { + if (sInstance == null) { + sInstance = new ManageCachePage(context); + } + return sInstance; + } + + public ManageCachePage(Context context) { + super(context); + Resources r = context.getResources(); + cacheBarHeight = r.getDimensionPixelSize(R.dimen.cache_bar_height); + cacheBarPinLeftMargin = r.getDimensionPixelSize(R.dimen.cache_bar_pin_left_margin); + cacheBarPinRightMargin = r.getDimensionPixelSize( + R.dimen.cache_bar_pin_right_margin); + cacheBarButtonRightMargin = r.getDimensionPixelSize( + R.dimen.cache_bar_button_right_margin); + cacheBarFontSize = r.getDimensionPixelSize(R.dimen.cache_bar_font_size); + } + } + + public static class PhotoPage { + private static PhotoPage sInstance; + + // These are all height values. See the comment in FilmStripView for + // the meaning of these values. + public final int filmstripTopMargin; + public final int filmstripMidMargin; + public final int filmstripBottomMargin; + public final int filmstripThumbSize; + public final int filmstripContentSize; + public final int filmstripGripSize; + public final int filmstripBarSize; + + // These are width values. + public final int filmstripGripWidth; + + public static synchronized PhotoPage get(Context context) { + if (sInstance == null) { + sInstance = new PhotoPage(context); + } + return sInstance; + } + + public PhotoPage(Context context) { + Resources r = context.getResources(); + filmstripTopMargin = r.getDimensionPixelSize(R.dimen.filmstrip_top_margin); + filmstripMidMargin = r.getDimensionPixelSize(R.dimen.filmstrip_mid_margin); + filmstripBottomMargin = r.getDimensionPixelSize(R.dimen.filmstrip_bottom_margin); + filmstripThumbSize = r.getDimensionPixelSize(R.dimen.filmstrip_thumb_size); + filmstripContentSize = r.getDimensionPixelSize(R.dimen.filmstrip_content_size); + filmstripGripSize = r.getDimensionPixelSize(R.dimen.filmstrip_grip_size); + filmstripBarSize = r.getDimensionPixelSize(R.dimen.filmstrip_bar_size); + filmstripGripWidth = r.getDimensionPixelSize(R.dimen.filmstrip_grip_width); + } + } +} + diff --git a/src/com/android/gallery3d/app/CropImage.java b/src/com/android/gallery3d/app/CropImage.java new file mode 100644 index 000000000..6c0a0c7eb --- /dev/null +++ b/src/com/android/gallery3d/app/CropImage.java @@ -0,0 +1,850 @@ +/* + * 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.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.LocalImage; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.picasasource.PicasaSource; +import com.android.gallery3d.ui.BitmapTileProvider; +import com.android.gallery3d.ui.CropView; +import com.android.gallery3d.ui.GLRoot; +import com.android.gallery3d.ui.SynchronizedHandler; +import com.android.gallery3d.ui.TileImageViewAdapter; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.InterruptableOutputStream; +import com.android.gallery3d.util.ThreadPool.CancelListener; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.app.ProgressDialog; +import android.app.WallpaperManager; +import android.content.ContentValues; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Message; +import android.provider.MediaStore; +import android.provider.MediaStore.Images; +import android.view.Menu; +import android.view.MenuItem; +import android.view.Window; +import android.widget.Toast; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * The activity can crop specific region of interest from an image. + */ +public class CropImage extends AbstractGalleryActivity { + private static final String TAG = "CropImage"; + public static final String ACTION_CROP = "com.android.camera.action.CROP"; + + private static final int MAX_PIXEL_COUNT = 5 * 1000000; // 5M pixels + private static final int MAX_FILE_INDEX = 1000; + private static final int TILE_SIZE = 512; + private static final int BACKUP_PIXEL_COUNT = 480000; // around 800x600 + + private static final int MSG_LARGE_BITMAP = 1; + private static final int MSG_BITMAP = 2; + private static final int MSG_SAVE_COMPLETE = 3; + + private static final int MAX_BACKUP_IMAGE_SIZE = 320; + private static final int DEFAULT_COMPRESS_QUALITY = 90; + + public static final String KEY_RETURN_DATA = "return-data"; + public static final String KEY_CROPPED_RECT = "cropped-rect"; + public static final String KEY_ASPECT_X = "aspectX"; + public static final String KEY_ASPECT_Y = "aspectY"; + public static final String KEY_SPOTLIGHT_X = "spotlightX"; + public static final String KEY_SPOTLIGHT_Y = "spotlightY"; + public static final String KEY_OUTPUT_X = "outputX"; + public static final String KEY_OUTPUT_Y = "outputY"; + public static final String KEY_SCALE = "scale"; + public static final String KEY_DATA = "data"; + public static final String KEY_SCALE_UP_IF_NEEDED = "scaleUpIfNeeded"; + public static final String KEY_OUTPUT_FORMAT = "outputFormat"; + public static final String KEY_SET_AS_WALLPAPER = "set-as-wallpaper"; + public static final String KEY_NO_FACE_DETECTION = "noFaceDetection"; + + private static final String KEY_STATE = "state"; + + private static final int STATE_INIT = 0; + private static final int STATE_LOADED = 1; + private static final int STATE_SAVING = 2; + + public static final String DOWNLOAD_STRING = "download"; + public static final File DOWNLOAD_BUCKET = new File( + Environment.getExternalStorageDirectory(), DOWNLOAD_STRING); + + public static final String CROP_ACTION = "com.android.camera.action.CROP"; + + private int mState = STATE_INIT; + + private CropView mCropView; + + private boolean mDoFaceDetection = true; + + private Handler mMainHandler; + + // We keep the following members so that we can free them + + // mBitmap is the unrotated bitmap we pass in to mCropView for detect faces. + // mCropView is responsible for rotating it to the way that it is viewed by users. + private Bitmap mBitmap; + private BitmapTileProvider mBitmapTileProvider; + private BitmapRegionDecoder mRegionDecoder; + private Bitmap mBitmapInIntent; + private boolean mUseRegionDecoder = false; + + private ProgressDialog mProgressDialog; + private Future<BitmapRegionDecoder> mLoadTask; + private Future<Bitmap> mLoadBitmapTask; + private Future<Intent> mSaveTask; + + private MediaItem mMediaItem; + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + requestWindowFeature(Window.FEATURE_ACTION_BAR); + requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); + + // Initialize UI + setContentView(R.layout.cropimage); + mCropView = new CropView(this); + getGLRoot().setContentPane(mCropView); + + mMainHandler = new SynchronizedHandler(getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_LARGE_BITMAP: { + mProgressDialog.dismiss(); + onBitmapRegionDecoderAvailable((BitmapRegionDecoder) message.obj); + break; + } + case MSG_BITMAP: { + mProgressDialog.dismiss(); + onBitmapAvailable((Bitmap) message.obj); + break; + } + case MSG_SAVE_COMPLETE: { + mProgressDialog.dismiss(); + setResult(RESULT_OK, (Intent) message.obj); + finish(); + break; + } + } + } + }; + + setCropParameters(); + } + + @Override + protected void onSaveInstanceState(Bundle saveState) { + saveState.putInt(KEY_STATE, mState); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.crop, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.cancel: { + setResult(RESULT_CANCELED); + finish(); + break; + } + case R.id.save: { + onSaveClicked(); + break; + } + } + return true; + } + + private class SaveOutput implements Job<Intent> { + private RectF mCropRect; + + public SaveOutput(RectF cropRect) { + mCropRect = cropRect; + } + + public Intent run(JobContext jc) { + RectF cropRect = mCropRect; + Bundle extra = getIntent().getExtras(); + + Rect rect = new Rect( + Math.round(cropRect.left), Math.round(cropRect.top), + Math.round(cropRect.right), Math.round(cropRect.bottom)); + + Intent result = new Intent(); + result.putExtra(KEY_CROPPED_RECT, rect); + Bitmap cropped = null; + boolean outputted = false; + if (extra != null) { + Uri uri = (Uri) extra.getParcelable(MediaStore.EXTRA_OUTPUT); + if (uri != null) { + if (jc.isCancelled()) return null; + outputted = true; + cropped = getCroppedImage(rect); + if (!saveBitmapToUri(jc, cropped, uri)) return null; + } + if (extra.getBoolean(KEY_RETURN_DATA, false)) { + if (jc.isCancelled()) return null; + outputted = true; + if (cropped == null) cropped = getCroppedImage(rect); + result.putExtra(KEY_DATA, cropped); + } + if (extra.getBoolean(KEY_SET_AS_WALLPAPER, false)) { + if (jc.isCancelled()) return null; + outputted = true; + if (cropped == null) cropped = getCroppedImage(rect); + if (!setAsWallpaper(jc, cropped)) return null; + } + } + if (!outputted) { + if (jc.isCancelled()) return null; + if (cropped == null) cropped = getCroppedImage(rect); + Uri data = saveToMediaProvider(jc, cropped); + if (data != null) result.setData(data); + } + return result; + } + } + + public static String determineCompressFormat(MediaObject obj) { + String compressFormat = "JPEG"; + if (obj instanceof MediaItem) { + String mime = ((MediaItem) obj).getMimeType(); + if (mime.contains("png") || mime.contains("gif")) { + // Set the compress format to PNG for png and gif images + // because they may contain alpha values. + compressFormat = "PNG"; + } + } + return compressFormat; + } + + private boolean setAsWallpaper(JobContext jc, Bitmap wallpaper) { + try { + WallpaperManager.getInstance(this).setBitmap(wallpaper); + } catch (IOException e) { + Log.w(TAG, "fail to set wall paper", e); + } + return true; + } + + private File saveMedia( + JobContext jc, Bitmap cropped, File directory, String filename) { + // Try file-1.jpg, file-2.jpg, ... until we find a filename + // which does not exist yet. + File candidate = null; + String fileExtension = getFileExtension(); + for (int i = 1; i < MAX_FILE_INDEX; ++i) { + candidate = new File(directory, filename + "-" + i + "." + + fileExtension); + try { + if (candidate.createNewFile()) break; + } catch (IOException e) { + Log.e(TAG, "fail to create new file: " + + candidate.getAbsolutePath(), e); + return null; + } + } + if (!candidate.exists() || !candidate.isFile()) { + throw new RuntimeException("cannot create file: " + filename); + } + + candidate.setReadable(true, false); + candidate.setWritable(true, false); + + try { + FileOutputStream fos = new FileOutputStream(candidate); + try { + saveBitmapToOutputStream(jc, cropped, + convertExtensionToCompressFormat(fileExtension), fos); + } finally { + fos.close(); + } + } catch (IOException e) { + Log.e(TAG, "fail to save image: " + + candidate.getAbsolutePath(), e); + candidate.delete(); + return null; + } + + if (jc.isCancelled()) { + candidate.delete(); + return null; + } + + return candidate; + } + + private Uri saveToMediaProvider(JobContext jc, Bitmap cropped) { + if (PicasaSource.isPicasaImage(mMediaItem)) { + return savePicasaImage(jc, cropped); + } else if (mMediaItem instanceof LocalImage) { + return saveLocalImage(jc, cropped); + } else { + Log.w(TAG, "no output for crop image " + mMediaItem); + return null; + } + } + + private Uri savePicasaImage(JobContext jc, Bitmap cropped) { + if (!DOWNLOAD_BUCKET.isDirectory() && !DOWNLOAD_BUCKET.mkdirs()) { + throw new RuntimeException("cannot create download folder"); + } + + String filename = PicasaSource.getImageTitle(mMediaItem); + int pos = filename.lastIndexOf('.'); + if (pos >= 0) filename = filename.substring(0, pos); + File output = saveMedia(jc, cropped, DOWNLOAD_BUCKET, filename); + if (output == null) return null; + + long now = System.currentTimeMillis() / 1000; + ContentValues values = new ContentValues(); + values.put(Images.Media.TITLE, PicasaSource.getImageTitle(mMediaItem)); + values.put(Images.Media.DISPLAY_NAME, output.getName()); + values.put(Images.Media.DATE_TAKEN, PicasaSource.getDateTaken(mMediaItem)); + values.put(Images.Media.DATE_MODIFIED, now); + values.put(Images.Media.DATE_ADDED, now); + values.put(Images.Media.MIME_TYPE, "image/jpeg"); + values.put(Images.Media.ORIENTATION, 0); + values.put(Images.Media.DATA, output.getAbsolutePath()); + values.put(Images.Media.SIZE, output.length()); + + double latitude = PicasaSource.getLatitude(mMediaItem); + double longitude = PicasaSource.getLongitude(mMediaItem); + if (GalleryUtils.isValidLocation(latitude, longitude)) { + values.put(Images.Media.LATITUDE, latitude); + values.put(Images.Media.LONGITUDE, longitude); + } + return getContentResolver().insert( + Images.Media.EXTERNAL_CONTENT_URI, values); + } + + private Uri saveLocalImage(JobContext jc, Bitmap cropped) { + LocalImage localImage = (LocalImage) mMediaItem; + + File oldPath = new File(localImage.filePath); + File directory = new File(oldPath.getParent()); + + String filename = oldPath.getName(); + int pos = filename.lastIndexOf('.'); + if (pos >= 0) filename = filename.substring(0, pos); + File output = saveMedia(jc, cropped, directory, filename); + if (output == null) return null; + + long now = System.currentTimeMillis() / 1000; + ContentValues values = new ContentValues(); + values.put(Images.Media.TITLE, localImage.caption); + values.put(Images.Media.DISPLAY_NAME, output.getName()); + values.put(Images.Media.DATE_TAKEN, localImage.dateTakenInMs); + values.put(Images.Media.DATE_MODIFIED, now); + values.put(Images.Media.DATE_ADDED, now); + values.put(Images.Media.MIME_TYPE, "image/jpeg"); + values.put(Images.Media.ORIENTATION, 0); + values.put(Images.Media.DATA, output.getAbsolutePath()); + values.put(Images.Media.SIZE, output.length()); + + if (GalleryUtils.isValidLocation(localImage.latitude, localImage.longitude)) { + values.put(Images.Media.LATITUDE, localImage.latitude); + values.put(Images.Media.LONGITUDE, localImage.longitude); + } + return getContentResolver().insert( + Images.Media.EXTERNAL_CONTENT_URI, values); + } + + private boolean saveBitmapToOutputStream( + JobContext jc, Bitmap bitmap, CompressFormat format, OutputStream os) { + // We wrap the OutputStream so that it can be interrupted. + final InterruptableOutputStream ios = new InterruptableOutputStream(os); + jc.setCancelListener(new CancelListener() { + public void onCancel() { + ios.interrupt(); + } + }); + try { + bitmap.compress(format, DEFAULT_COMPRESS_QUALITY, os); + if (!jc.isCancelled()) return false; + } finally { + jc.setCancelListener(null); + Utils.closeSilently(os); + } + return false; + } + + private boolean saveBitmapToUri(JobContext jc, Bitmap bitmap, Uri uri) { + try { + return saveBitmapToOutputStream(jc, bitmap, + convertExtensionToCompressFormat(getFileExtension()), + getContentResolver().openOutputStream(uri)); + } catch (FileNotFoundException e) { + Log.w(TAG, "cannot write output", e); + } + return true; + } + + private CompressFormat convertExtensionToCompressFormat(String extension) { + return extension.equals("png") + ? CompressFormat.PNG + : CompressFormat.JPEG; + } + + private String getFileExtension() { + String requestFormat = getIntent().getStringExtra(KEY_OUTPUT_FORMAT); + String outputFormat = (requestFormat == null) + ? determineCompressFormat(mMediaItem) + : requestFormat; + + outputFormat = outputFormat.toLowerCase(); + return (outputFormat.equals("png") || outputFormat.equals("gif")) + ? "png" // We don't support gif compression. + : "jpg"; + } + + private void onSaveClicked() { + Bundle extra = getIntent().getExtras(); + RectF cropRect = mCropView.getCropRectangle(); + if (cropRect == null) return; + mState = STATE_SAVING; + int messageId = extra != null && extra.getBoolean(KEY_SET_AS_WALLPAPER) + ? R.string.wallpaper + : R.string.saving_image; + mProgressDialog = ProgressDialog.show( + this, null, getString(messageId), true, false); + mSaveTask = getThreadPool().submit(new SaveOutput(cropRect), + new FutureListener<Intent>() { + public void onFutureDone(Future<Intent> future) { + mSaveTask = null; + if (future.get() == null) return; + mMainHandler.sendMessage(mMainHandler.obtainMessage( + MSG_SAVE_COMPLETE, future.get())); + } + }); + } + + private Bitmap getCroppedImage(Rect rect) { + Utils.assertTrue(rect.width() > 0 && rect.height() > 0); + + Bundle extras = getIntent().getExtras(); + // (outputX, outputY) = the width and height of the returning bitmap. + int outputX = rect.width(); + int outputY = rect.height(); + if (extras != null) { + outputX = extras.getInt(KEY_OUTPUT_X, outputX); + outputY = extras.getInt(KEY_OUTPUT_Y, outputY); + } + + if (outputX * outputY > MAX_PIXEL_COUNT) { + float scale = (float) Math.sqrt( + (double) MAX_PIXEL_COUNT / outputX / outputY); + Log.w(TAG, "scale down the cropped image: " + scale); + outputX = Math.round(scale * outputX); + outputY = Math.round(scale * outputY); + } + + // (rect.width() * scaleX, rect.height() * scaleY) = + // the size of drawing area in output bitmap + float scaleX = 1; + float scaleY = 1; + Rect dest = new Rect(0, 0, outputX, outputY); + if (extras == null || extras.getBoolean(KEY_SCALE, true)) { + scaleX = (float) outputX / rect.width(); + scaleY = (float) outputY / rect.height(); + if (extras == null || !extras.getBoolean( + KEY_SCALE_UP_IF_NEEDED, false)) { + if (scaleX > 1f) scaleX = 1; + if (scaleY > 1f) scaleY = 1; + } + } + + // Keep the content in the center (or crop the content) + int rectWidth = Math.round(rect.width() * scaleX); + int rectHeight = Math.round(rect.height() * scaleY); + dest.set(Math.round((outputX - rectWidth) / 2f), + Math.round((outputY - rectHeight) / 2f), + Math.round((outputX + rectWidth) / 2f), + Math.round((outputY + rectHeight) / 2f)); + + if (mBitmapInIntent != null) { + Bitmap source = mBitmapInIntent; + Bitmap result = Bitmap.createBitmap( + outputX, outputY, Config.ARGB_8888); + Canvas canvas = new Canvas(result); + canvas.drawBitmap(source, rect, dest, null); + return result; + } + + int rotation = mMediaItem.getRotation(); + rotateRectangle(rect, mCropView.getImageWidth(), + mCropView.getImageHeight(), 360 - rotation); + rotateRectangle(dest, outputX, outputY, 360 - rotation); + if (mUseRegionDecoder) { + BitmapFactory.Options options = new BitmapFactory.Options(); + int sample = BitmapUtils.computeSampleSizeLarger( + Math.max(scaleX, scaleY)); + options.inSampleSize = sample; + if ((rect.width() / sample) == dest.width() + && (rect.height() / sample) == dest.height() + && rotation == 0) { + // To prevent concurrent access in GLThread + synchronized (mRegionDecoder) { + return mRegionDecoder.decodeRegion(rect, options); + } + } + Bitmap result = Bitmap.createBitmap( + outputX, outputY, Config.ARGB_8888); + Canvas canvas = new Canvas(result); + rotateCanvas(canvas, outputX, outputY, rotation); + drawInTiles(canvas, mRegionDecoder, rect, dest, sample); + return result; + } else { + Bitmap result = Bitmap.createBitmap(outputX, outputY, Config.ARGB_8888); + Canvas canvas = new Canvas(result); + rotateCanvas(canvas, outputX, outputY, rotation); + canvas.drawBitmap(mBitmap, + rect, dest, new Paint(Paint.FILTER_BITMAP_FLAG)); + return result; + } + } + + private static void rotateCanvas( + Canvas canvas, int width, int height, int rotation) { + canvas.translate(width / 2, height / 2); + canvas.rotate(rotation); + if (((rotation / 90) & 0x01) == 0) { + canvas.translate(-width / 2, -height / 2); + } else { + canvas.translate(-height / 2, -width / 2); + } + } + + private static void rotateRectangle( + Rect rect, int width, int height, int rotation) { + if (rotation == 0 || rotation == 360) return; + + int w = rect.width(); + int h = rect.height(); + switch (rotation) { + case 90: { + rect.top = rect.left; + rect.left = height - rect.bottom; + rect.right = rect.left + h; + rect.bottom = rect.top + w; + return; + } + case 180: { + rect.left = width - rect.right; + rect.top = height - rect.bottom; + rect.right = rect.left + w; + rect.bottom = rect.top + h; + return; + } + case 270: { + rect.left = rect.top; + rect.top = width - rect.right; + rect.right = rect.left + h; + rect.bottom = rect.top + w; + return; + } + default: throw new AssertionError(); + } + } + + private void drawInTiles(Canvas canvas, + BitmapRegionDecoder decoder, Rect rect, Rect dest, int sample) { + int tileSize = TILE_SIZE * sample; + Rect tileRect = new Rect(); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Config.ARGB_8888; + options.inSampleSize = sample; + canvas.translate(dest.left, dest.top); + canvas.scale((float) sample * dest.width() / rect.width(), + (float) sample * dest.height() / rect.height()); + Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG); + for (int tx = rect.left, x = 0; + tx < rect.right; tx += tileSize, x += TILE_SIZE) { + for (int ty = rect.top, y = 0; + ty < rect.bottom; ty += tileSize, y += TILE_SIZE) { + tileRect.set(tx, ty, tx + tileSize, ty + tileSize); + if (tileRect.intersect(rect)) { + Bitmap bitmap; + + // To prevent concurrent access in GLThread + synchronized (decoder) { + bitmap = decoder.decodeRegion(tileRect, options); + } + canvas.drawBitmap(bitmap, x, y, paint); + bitmap.recycle(); + } + } + } + } + + private void onBitmapRegionDecoderAvailable( + BitmapRegionDecoder regionDecoder) { + + if (regionDecoder == null) { + Toast.makeText(this, "fail to load image", Toast.LENGTH_SHORT).show(); + finish(); + return; + } + mRegionDecoder = regionDecoder; + mUseRegionDecoder = true; + mState = STATE_LOADED; + + BitmapFactory.Options options = new BitmapFactory.Options(); + int width = regionDecoder.getWidth(); + int height = regionDecoder.getHeight(); + options.inSampleSize = BitmapUtils.computeSampleSize(width, height, + BitmapUtils.UNCONSTRAINED, BACKUP_PIXEL_COUNT); + mBitmap = regionDecoder.decodeRegion( + new Rect(0, 0, width, height), options); + mCropView.setDataModel(new TileImageViewAdapter( + mBitmap, regionDecoder), mMediaItem.getRotation()); + if (mDoFaceDetection) { + mCropView.detectFaces(mBitmap); + } else { + mCropView.initializeHighlightRectangle(); + } + } + + private void onBitmapAvailable(Bitmap bitmap) { + if (bitmap == null) { + Toast.makeText(this, "fail to load image", Toast.LENGTH_SHORT).show(); + finish(); + return; + } + mUseRegionDecoder = false; + mState = STATE_LOADED; + + mBitmap = bitmap; + BitmapFactory.Options options = new BitmapFactory.Options(); + mCropView.setDataModel(new BitmapTileProvider(bitmap, 512), + mMediaItem.getRotation()); + if (mDoFaceDetection) { + mCropView.detectFaces(bitmap); + } else { + mCropView.initializeHighlightRectangle(); + } + } + + private void setCropParameters() { + Bundle extras = getIntent().getExtras(); + if (extras == null) + return; + int aspectX = extras.getInt(KEY_ASPECT_X, 0); + int aspectY = extras.getInt(KEY_ASPECT_Y, 0); + if (aspectX != 0 && aspectY != 0) { + mCropView.setAspectRatio((float) aspectX / aspectY); + } + + float spotlightX = extras.getFloat(KEY_SPOTLIGHT_X, 0); + float spotlightY = extras.getFloat(KEY_SPOTLIGHT_Y, 0); + if (spotlightX != 0 && spotlightY != 0) { + mCropView.setSpotlightRatio(spotlightX, spotlightY); + } + } + + private void initializeData() { + Bundle extras = getIntent().getExtras(); + + if (extras != null) { + if (extras.containsKey(KEY_NO_FACE_DETECTION)) { + mDoFaceDetection = !extras.getBoolean(KEY_NO_FACE_DETECTION); + } + + mBitmapInIntent = extras.getParcelable(KEY_DATA); + + if (mBitmapInIntent != null) { + mBitmapTileProvider = + new BitmapTileProvider(mBitmapInIntent, MAX_BACKUP_IMAGE_SIZE); + mCropView.setDataModel(mBitmapTileProvider, 0); + if (mDoFaceDetection) { + mCropView.detectFaces(mBitmapInIntent); + } else { + mCropView.initializeHighlightRectangle(); + } + mState = STATE_LOADED; + return; + } + } + + mProgressDialog = ProgressDialog.show( + this, null, getString(R.string.loading_image), true, false); + + mMediaItem = getMediaItemFromIntentData(); + if (mMediaItem == null) return; + + boolean supportedByBitmapRegionDecoder = + (mMediaItem.getSupportedOperations() & MediaItem.SUPPORT_FULL_IMAGE) != 0; + if (supportedByBitmapRegionDecoder) { + mLoadTask = getThreadPool().submit(new LoadDataTask(mMediaItem), + new FutureListener<BitmapRegionDecoder>() { + public void onFutureDone(Future<BitmapRegionDecoder> future) { + mLoadTask = null; + BitmapRegionDecoder decoder = future.get(); + if (future.isCancelled()) { + if (decoder != null) decoder.recycle(); + return; + } + mMainHandler.sendMessage(mMainHandler.obtainMessage( + MSG_LARGE_BITMAP, decoder)); + } + }); + } else { + mLoadBitmapTask = getThreadPool().submit(new LoadBitmapDataTask(mMediaItem), + new FutureListener<Bitmap>() { + public void onFutureDone(Future<Bitmap> future) { + mLoadBitmapTask = null; + Bitmap bitmap = future.get(); + if (future.isCancelled()) { + if (bitmap != null) bitmap.recycle(); + return; + } + mMainHandler.sendMessage(mMainHandler.obtainMessage( + MSG_BITMAP, bitmap)); + } + }); + } + } + + @Override + protected void onResume() { + super.onResume(); + if (mState == STATE_INIT) initializeData(); + if (mState == STATE_SAVING) onSaveClicked(); + + // TODO: consider to do it in GLView system + GLRoot root = getGLRoot(); + root.lockRenderThread(); + try { + mCropView.resume(); + } finally { + root.unlockRenderThread(); + } + } + + @Override + protected void onPause() { + super.onPause(); + + Future<BitmapRegionDecoder> loadTask = mLoadTask; + if (loadTask != null && !loadTask.isDone()) { + // load in progress, try to cancel it + loadTask.cancel(); + loadTask.waitDone(); + mProgressDialog.dismiss(); + } + + Future<Bitmap> loadBitmapTask = mLoadBitmapTask; + if (loadBitmapTask != null && !loadBitmapTask.isDone()) { + // load in progress, try to cancel it + loadBitmapTask.cancel(); + loadBitmapTask.waitDone(); + mProgressDialog.dismiss(); + } + + Future<Intent> saveTask = mSaveTask; + if (saveTask != null && !saveTask.isDone()) { + // save in progress, try to cancel it + saveTask.cancel(); + saveTask.waitDone(); + mProgressDialog.dismiss(); + } + GLRoot root = getGLRoot(); + root.lockRenderThread(); + try { + mCropView.pause(); + } finally { + root.unlockRenderThread(); + } + } + + private MediaItem getMediaItemFromIntentData() { + Uri uri = getIntent().getData(); + DataManager manager = getDataManager(); + if (uri == null) { + Log.w(TAG, "no data given"); + return null; + } + Path path = manager.findPathByUri(uri); + if (path == null) { + Log.w(TAG, "cannot get path for: " + uri); + return null; + } + return (MediaItem) manager.getMediaObject(path); + } + + private class LoadDataTask implements Job<BitmapRegionDecoder> { + MediaItem mItem; + + public LoadDataTask(MediaItem item) { + mItem = item; + } + + public BitmapRegionDecoder run(JobContext jc) { + return mItem == null ? null : mItem.requestLargeImage().run(jc); + } + } + + private class LoadBitmapDataTask implements Job<Bitmap> { + MediaItem mItem; + + public LoadBitmapDataTask(MediaItem item) { + mItem = item; + } + public Bitmap run(JobContext jc) { + return mItem == null + ? null + : mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc); + } + } +} diff --git a/src/com/android/gallery3d/app/DialogPicker.java b/src/com/android/gallery3d/app/DialogPicker.java new file mode 100644 index 000000000..ebfc52158 --- /dev/null +++ b/src/com/android/gallery3d/app/DialogPicker.java @@ -0,0 +1,68 @@ +/* + * 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.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.ui.GLRoot; +import com.android.gallery3d.ui.GLRootView; +import com.android.gallery3d.util.GalleryUtils; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; + +public class DialogPicker extends AbstractGalleryActivity + implements OnClickListener { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.dialog_picker); + ((GLRootView) findViewById(R.id.gl_root_view)).setZOrderOnTop(true); + findViewById(R.id.cancel).setOnClickListener(this); + + int typeBits = GalleryUtils.determineTypeBits(this, getIntent()); + setTitle(GalleryUtils.getSelectionModePrompt(typeBits)); + Intent intent = getIntent(); + Bundle extras = intent.getExtras(); + Bundle data = extras == null ? new Bundle() : new Bundle(extras); + + data.putBoolean(Gallery.KEY_GET_CONTENT, true); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, + getDataManager().getTopSetPath(typeBits)); + getStateManager().startState(AlbumSetPage.class, data); + } + + @Override + public void onBackPressed() { + // send the back event to the top sub-state + GLRoot root = getGLRoot(); + root.lockRenderThread(); + try { + getStateManager().getTopState().onBackPressed(); + } finally { + root.unlockRenderThread(); + } + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.cancel) finish(); + } +} diff --git a/src/com/android/gallery3d/app/EyePosition.java b/src/com/android/gallery3d/app/EyePosition.java new file mode 100644 index 000000000..1c3aa60bb --- /dev/null +++ b/src/com/android/gallery3d/app/EyePosition.java @@ -0,0 +1,218 @@ +/* + * 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.app; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.GalleryUtils; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.SystemClock; +import android.view.Display; +import android.view.Surface; +import android.view.WindowManager; + +public class EyePosition { + private static final String TAG = "EyePosition"; + + public interface EyePositionListener { + public void onEyePositionChanged(float x, float y, float z); + } + + private static final float GYROSCOPE_THRESHOLD = 0.15f; + private static final float GYROSCOPE_LIMIT = 10f; + private static final int GYROSCOPE_SETTLE_DOWN = 15; + private static final float GYROSCOPE_RESTORE_FACTOR = 0.995f; + + private static final double USER_ANGEL = Math.toRadians(10); + private static final float USER_ANGEL_COS = (float) Math.cos(USER_ANGEL); + private static final float USER_ANGEL_SIN = (float) Math.sin(USER_ANGEL); + private static final float MAX_VIEW_RANGE = (float) 0.5; + private static final int NOT_STARTED = -1; + + private static final float USER_DISTANCE_METER = 0.3f; + + private Context mContext; + private EyePositionListener mListener; + private Display mDisplay; + // The eyes' position of the user, the origin is at the center of the + // device and the unit is in pixels. + private float mX; + private float mY; + private float mZ; + + private final float mUserDistance; // in pixel + private final float mLimit; + private long mStartTime = NOT_STARTED; + private Sensor mSensor; + private PositionListener mPositionListener = new PositionListener(); + + private int mGyroscopeCountdown = 0; + + public EyePosition(Context context, EyePositionListener listener) { + mContext = context; + mListener = listener; + mUserDistance = GalleryUtils.meterToPixel(USER_DISTANCE_METER); + mLimit = mUserDistance * MAX_VIEW_RANGE; + + WindowManager wManager = (WindowManager) mContext + .getSystemService(Context.WINDOW_SERVICE); + mDisplay = wManager.getDefaultDisplay(); + + SensorManager sManager = (SensorManager) mContext + .getSystemService(Context.SENSOR_SERVICE); + mSensor = sManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); + if (mSensor == null) { + Log.w(TAG, "no gyroscope, use accelerometer instead"); + mSensor = sManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + } + if (mSensor == null) { + Log.w(TAG, "no sensor available"); + } + } + + public void resetPosition() { + mStartTime = NOT_STARTED; + mX = mY = 0; + mZ = -mUserDistance; + mListener.onEyePositionChanged(mX, mY, mZ); + } + + /* + * We assume the user is at the following position + * + * /|\ user's eye + * | / + * -G(gravity) | / + * |_/ + * / |/_____\ -Y (-y direction of device) + * user angel + */ + private void onAccelerometerChanged(float gx, float gy, float gz) { + + float x = gx, y = gy, z = gz; + + switch (mDisplay.getRotation()) { + case Surface.ROTATION_90: x = -gy; y= gx; break; + case Surface.ROTATION_180: x = -gx; y = -gy; break; + case Surface.ROTATION_270: x = gy; y = -gx; break; + } + + float temp = x * x + y * y + z * z; + float t = -y /temp; + + float tx = t * x; + float ty = -1 + t * y; + float tz = t * z; + + float length = (float) Math.sqrt(tx * tx + ty * ty + tz * tz); + float glength = (float) Math.sqrt(temp); + + mX = Utils.clamp((x * USER_ANGEL_COS / glength + + tx * USER_ANGEL_SIN / length) * mUserDistance, + -mLimit, mLimit); + mY = -Utils.clamp((y * USER_ANGEL_COS / glength + + ty * USER_ANGEL_SIN / length) * mUserDistance, + -mLimit, mLimit); + mZ = (float) -Math.sqrt( + mUserDistance * mUserDistance - mX * mX - mY * mY); + mListener.onEyePositionChanged(mX, mY, mZ); + } + + private void onGyroscopeChanged(float gx, float gy, float gz) { + long now = SystemClock.elapsedRealtime(); + float distance = (gx > 0 ? gx : -gx) + (gy > 0 ? gy : - gy); + if (distance < GYROSCOPE_THRESHOLD + || distance > GYROSCOPE_LIMIT || mGyroscopeCountdown > 0) { + --mGyroscopeCountdown; + mStartTime = now; + float limit = mUserDistance / 20f; + if (mX > limit || mX < -limit || mY > limit || mY < -limit) { + mX *= GYROSCOPE_RESTORE_FACTOR; + mY *= GYROSCOPE_RESTORE_FACTOR; + mZ = (float) -Math.sqrt( + mUserDistance * mUserDistance - mX * mX - mY * mY); + mListener.onEyePositionChanged(mX, mY, mZ); + } + return; + } + + float t = (now - mStartTime) / 1000f * mUserDistance * (-mZ); + mStartTime = now; + + float x = -gy, y = -gx; + switch (mDisplay.getRotation()) { + case Surface.ROTATION_90: x = -gx; y= gy; break; + case Surface.ROTATION_180: x = gy; y = gx; break; + case Surface.ROTATION_270: x = gx; y = -gy; break; + } + + mX = Utils.clamp((float) (mX + x * t / Math.hypot(mZ, mX)), + -mLimit, mLimit) * GYROSCOPE_RESTORE_FACTOR; + mY = Utils.clamp((float) (mY + y * t / Math.hypot(mZ, mY)), + -mLimit, mLimit) * GYROSCOPE_RESTORE_FACTOR; + + mZ = (float) -Math.sqrt( + mUserDistance * mUserDistance - mX * mX - mY * mY); + mListener.onEyePositionChanged(mX, mY, mZ); + } + + private class PositionListener implements SensorEventListener { + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + + public void onSensorChanged(SensorEvent event) { + switch (event.sensor.getType()) { + case Sensor.TYPE_GYROSCOPE: { + onGyroscopeChanged( + event.values[0], event.values[1], event.values[2]); + break; + } + case Sensor.TYPE_ACCELEROMETER: { + onAccelerometerChanged( + event.values[0], event.values[1], event.values[2]); + } + } + } + } + + public void pause() { + if (mSensor != null) { + SensorManager sManager = (SensorManager) mContext + .getSystemService(Context.SENSOR_SERVICE); + sManager.unregisterListener(mPositionListener); + } + } + + public void resume() { + if (mSensor != null) { + SensorManager sManager = (SensorManager) mContext + .getSystemService(Context.SENSOR_SERVICE); + sManager.registerListener(mPositionListener, + mSensor, SensorManager.SENSOR_DELAY_GAME); + } + + mStartTime = NOT_STARTED; + mGyroscopeCountdown = GYROSCOPE_SETTLE_DOWN; + mX = mY = 0; + mZ = -mUserDistance; + mListener.onEyePositionChanged(mX, mY, mZ); + } +} diff --git a/src/com/android/gallery3d/app/FilterUtils.java b/src/com/android/gallery3d/app/FilterUtils.java new file mode 100644 index 000000000..9b8ea2d62 --- /dev/null +++ b/src/com/android/gallery3d/app/FilterUtils.java @@ -0,0 +1,296 @@ +/* + * 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.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.Path; + +// This class handles filtering and clustering. +// +// We allow at most only one filter operation at a time (Currently it +// doesn't make sense to use more than one). Also each clustering operation +// can be applied at most once. In addition, there is one more constraint +// ("fixed set constraint") described below. +// +// A clustered album (not including album set) and its base sets are fixed. +// For example, +// +// /cluster/{base_set}/time/7 +// +// This set and all sets inside base_set (recursively) are fixed because +// 1. We can not change this set to use another clustering condition (like +// changing "time" to "location"). +// 2. Neither can we change any set in the base_set. +// The reason is in both cases the 7th set may not exist in the new clustering. +// --------------------- +// newPath operation: create a new path based on a source path and put an extra +// condition on top of it: +// +// T = newFilterPath(S, filterType); +// T = newClusterPath(S, clusterType); +// +// Similar functions can be used to replace the current condition (if there is one). +// +// T = switchFilterPath(S, filterType); +// T = switchClusterPath(S, clusterType); +// +// For all fixed set in the path defined above, if some clusterType and +// filterType are already used, they cannot not be used as parameter for these +// functions. setupMenuItems() makes sure those types cannot be selected. +// +public class FilterUtils { + private static final String TAG = "FilterUtils"; + + public static final int CLUSTER_BY_ALBUM = 1; + public static final int CLUSTER_BY_TIME = 2; + public static final int CLUSTER_BY_LOCATION = 4; + public static final int CLUSTER_BY_TAG = 8; + public static final int CLUSTER_BY_SIZE = 16; + public static final int CLUSTER_BY_FACE = 32; + + public static final int FILTER_IMAGE_ONLY = 1; + public static final int FILTER_VIDEO_ONLY = 2; + public static final int FILTER_ALL = 4; + + // These are indices of the return values of getAppliedFilters(). + // The _F suffix means "fixed". + private static final int CLUSTER_TYPE = 0; + private static final int FILTER_TYPE = 1; + private static final int CLUSTER_TYPE_F = 2; + private static final int FILTER_TYPE_F = 3; + private static final int CLUSTER_CURRENT_TYPE = 4; + private static final int FILTER_CURRENT_TYPE = 5; + + public static void setupMenuItems(GalleryActionBar model, Path path, boolean inAlbum) { + int[] result = new int[6]; + getAppliedFilters(path, result); + int ctype = result[CLUSTER_TYPE]; + int ftype = result[FILTER_TYPE]; + int ftypef = result[FILTER_TYPE_F]; + int ccurrent = result[CLUSTER_CURRENT_TYPE]; + int fcurrent = result[FILTER_CURRENT_TYPE]; + + setMenuItemApplied(model, CLUSTER_BY_TIME, + (ctype & CLUSTER_BY_TIME) != 0, (ccurrent & CLUSTER_BY_TIME) != 0); + setMenuItemApplied(model, CLUSTER_BY_LOCATION, + (ctype & CLUSTER_BY_LOCATION) != 0, (ccurrent & CLUSTER_BY_LOCATION) != 0); + setMenuItemApplied(model, CLUSTER_BY_TAG, + (ctype & CLUSTER_BY_TAG) != 0, (ccurrent & CLUSTER_BY_TAG) != 0); + setMenuItemApplied(model, CLUSTER_BY_FACE, + (ctype & CLUSTER_BY_FACE) != 0, (ccurrent & CLUSTER_BY_FACE) != 0); + + model.setClusterItemVisibility(CLUSTER_BY_ALBUM, !inAlbum || ctype == 0); + + setMenuItemApplied(model, R.id.action_cluster_album, ctype == 0, + ccurrent == 0); + + // A filtering is available if it's not applied, and the old filtering + // (if any) is not fixed. + setMenuItemAppliedEnabled(model, R.string.show_images_only, + (ftype & FILTER_IMAGE_ONLY) != 0, + (ftype & FILTER_IMAGE_ONLY) == 0 && ftypef == 0, + (fcurrent & FILTER_IMAGE_ONLY) != 0); + setMenuItemAppliedEnabled(model, R.string.show_videos_only, + (ftype & FILTER_VIDEO_ONLY) != 0, + (ftype & FILTER_VIDEO_ONLY) == 0 && ftypef == 0, + (fcurrent & FILTER_VIDEO_ONLY) != 0); + setMenuItemAppliedEnabled(model, R.string.show_all, + ftype == 0, ftype != 0 && ftypef == 0, fcurrent == 0); + } + + // Gets the filters applied in the path. + private static void getAppliedFilters(Path path, int[] result) { + getAppliedFilters(path, result, false); + } + + private static void getAppliedFilters(Path path, int[] result, boolean underCluster) { + String[] segments = path.split(); + // Recurse into sub media sets. + for (int i = 0; i < segments.length; i++) { + if (segments[i].startsWith("{")) { + String[] sets = Path.splitSequence(segments[i]); + for (int j = 0; j < sets.length; j++) { + Path sub = Path.fromString(sets[j]); + getAppliedFilters(sub, result, underCluster); + } + } + } + + // update current selection + if (segments[0].equals("cluster")) { + // if this is a clustered album, set underCluster to true. + if (segments.length == 4) { + underCluster = true; + } + + int ctype = toClusterType(segments[2]); + result[CLUSTER_TYPE] |= ctype; + result[CLUSTER_CURRENT_TYPE] = ctype; + if (underCluster) { + result[CLUSTER_TYPE_F] |= ctype; + } + } + } + + private static int toClusterType(String s) { + if (s.equals("time")) { + return CLUSTER_BY_TIME; + } else if (s.equals("location")) { + return CLUSTER_BY_LOCATION; + } else if (s.equals("tag")) { + return CLUSTER_BY_TAG; + } else if (s.equals("size")) { + return CLUSTER_BY_SIZE; + } else if (s.equals("face")) { + return CLUSTER_BY_FACE; + } + return 0; + } + + private static void setMenuItemApplied( + GalleryActionBar model, int id, boolean applied, boolean updateTitle) { + model.setClusterItemEnabled(id, !applied); + } + + private static void setMenuItemAppliedEnabled(GalleryActionBar model, int id, boolean applied, boolean enabled, boolean updateTitle) { + model.setClusterItemEnabled(id, enabled); + } + + // Add a specified filter to the path. + public static String newFilterPath(String base, int filterType) { + int mediaType; + switch (filterType) { + case FILTER_IMAGE_ONLY: + mediaType = MediaObject.MEDIA_TYPE_IMAGE; + break; + case FILTER_VIDEO_ONLY: + mediaType = MediaObject.MEDIA_TYPE_VIDEO; + break; + default: /* FILTER_ALL */ + return base; + } + + return "/filter/mediatype/" + mediaType + "/{" + base + "}"; + } + + // Add a specified clustering to the path. + public static String newClusterPath(String base, int clusterType) { + String kind; + switch (clusterType) { + case CLUSTER_BY_TIME: + kind = "time"; + break; + case CLUSTER_BY_LOCATION: + kind = "location"; + break; + case CLUSTER_BY_TAG: + kind = "tag"; + break; + case CLUSTER_BY_SIZE: + kind = "size"; + break; + case CLUSTER_BY_FACE: + kind = "face"; + break; + default: /* CLUSTER_BY_ALBUM */ + return base; + } + + return "/cluster/{" + base + "}/" + kind; + } + + // Change the topmost filter to the specified type. + public static String switchFilterPath(String base, int filterType) { + return newFilterPath(removeOneFilterFromPath(base), filterType); + } + + // Change the topmost clustering to the specified type. + public static String switchClusterPath(String base, int clusterType) { + return newClusterPath(removeOneClusterFromPath(base), clusterType); + } + + // Remove the topmost clustering (if any) from the path. + private static String removeOneClusterFromPath(String base) { + boolean[] done = new boolean[1]; + return removeOneClusterFromPath(base, done); + } + + private static String removeOneClusterFromPath(String base, boolean[] done) { + if (done[0]) return base; + + String[] segments = Path.split(base); + if (segments[0].equals("cluster")) { + done[0] = true; + return Path.splitSequence(segments[1])[0]; + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < segments.length; i++) { + sb.append("/"); + if (segments[i].startsWith("{")) { + sb.append("{"); + String[] sets = Path.splitSequence(segments[i]); + for (int j = 0; j < sets.length; j++) { + if (j > 0) { + sb.append(","); + } + sb.append(removeOneClusterFromPath(sets[j], done)); + } + sb.append("}"); + } else { + sb.append(segments[i]); + } + } + return sb.toString(); + } + + // Remove the topmost filter (if any) from the path. + private static String removeOneFilterFromPath(String base) { + boolean[] done = new boolean[1]; + return removeOneFilterFromPath(base, done); + } + + private static String removeOneFilterFromPath(String base, boolean[] done) { + if (done[0]) return base; + + String[] segments = Path.split(base); + if (segments[0].equals("filter") && segments[1].equals("mediatype")) { + done[0] = true; + return Path.splitSequence(segments[3])[0]; + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < segments.length; i++) { + sb.append("/"); + if (segments[i].startsWith("{")) { + sb.append("{"); + String[] sets = Path.splitSequence(segments[i]); + for (int j = 0; j < sets.length; j++) { + if (j > 0) { + sb.append(","); + } + sb.append(removeOneFilterFromPath(sets[j], done)); + } + sb.append("}"); + } else { + sb.append(segments[i]); + } + } + return sb.toString(); + } +} diff --git a/src/com/android/gallery3d/app/Gallery.java b/src/com/android/gallery3d/app/Gallery.java new file mode 100644 index 000000000..2c5263b03 --- /dev/null +++ b/src/com/android/gallery3d/app/Gallery.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2009 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.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.GLRoot; +import com.android.gallery3d.util.GalleryUtils; + +import android.app.ActionBar; +import android.content.ContentResolver; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.Window; +import android.widget.Toast; + +public final class Gallery extends AbstractGalleryActivity { + public static final String EXTRA_SLIDESHOW = "slideshow"; + public static final String EXTRA_CROP = "crop"; + + public static final String ACTION_REVIEW = "com.android.camera.action.REVIEW"; + public static final String KEY_GET_CONTENT = "get-content"; + public static final String KEY_GET_ALBUM = "get-album"; + public static final String KEY_TYPE_BITS = "type-bits"; + public static final String KEY_MEDIA_TYPES = "mediaTypes"; + + private static final String TAG = "Gallery"; + private GalleryActionBar mActionBar; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_ACTION_BAR); + requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + + setContentView(R.layout.main); + mActionBar = new GalleryActionBar(this); + + if (savedInstanceState != null) { + getStateManager().restoreFromState(savedInstanceState); + } else { + initializeByIntent(); + } + } + + private void initializeByIntent() { + Intent intent = getIntent(); + String action = intent.getAction(); + + if (Intent.ACTION_GET_CONTENT.equalsIgnoreCase(action)) { + startGetContent(intent); + } else if (Intent.ACTION_PICK.equalsIgnoreCase(action)) { + // We do NOT really support the PICK intent. Handle it as + // the GET_CONTENT. However, we need to translate the type + // in the intent here. + Log.w(TAG, "action PICK is not supported"); + String type = Utils.ensureNotNull(intent.getType()); + if (type.startsWith("vnd.android.cursor.dir/")) { + if (type.endsWith("/image")) intent.setType("image/*"); + if (type.endsWith("/video")) intent.setType("video/*"); + } + startGetContent(intent); + } else if (Intent.ACTION_VIEW.equalsIgnoreCase(action) + || ACTION_REVIEW.equalsIgnoreCase(action)){ + startViewAction(intent); + } else { + startDefaultPage(); + } + } + + public void startDefaultPage() { + Bundle data = new Bundle(); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, + getDataManager().getTopSetPath(DataManager.INCLUDE_ALL)); + getStateManager().startState(AlbumSetPage.class, data); + } + + private void startGetContent(Intent intent) { + Bundle data = intent.getExtras() != null + ? new Bundle(intent.getExtras()) + : new Bundle(); + data.putBoolean(KEY_GET_CONTENT, true); + int typeBits = GalleryUtils.determineTypeBits(this, intent); + data.putInt(KEY_TYPE_BITS, typeBits); + data.putString(AlbumSetPage.KEY_MEDIA_PATH, + getDataManager().getTopSetPath(typeBits)); + getStateManager().startState(AlbumSetPage.class, data); + } + + private String getContentType(Intent intent) { + String type = intent.getType(); + if (type != null) return type; + + Uri uri = intent.getData(); + try { + return getContentResolver().getType(uri); + } catch (Throwable t) { + Log.w(TAG, "get type fail", t); + return null; + } + } + + private void startViewAction(Intent intent) { + Boolean slideshow = intent.getBooleanExtra(EXTRA_SLIDESHOW, false); + if (slideshow) { + getActionBar().hide(); + DataManager manager = getDataManager(); + Path path = manager.findPathByUri(intent.getData()); + if (path == null || manager.getMediaObject(path) + instanceof MediaItem) { + path = Path.fromString( + manager.getTopSetPath(DataManager.INCLUDE_IMAGE)); + } + Bundle data = new Bundle(); + data.putString(SlideshowPage.KEY_SET_PATH, path.toString()); + data.putBoolean(SlideshowPage.KEY_RANDOM_ORDER, true); + data.putBoolean(SlideshowPage.KEY_REPEAT, true); + getStateManager().startState(SlideshowPage.class, data); + } else { + Bundle data = new Bundle(); + DataManager dm = getDataManager(); + Uri uri = intent.getData(); + String contentType = getContentType(intent); + if (contentType == null) { + Toast.makeText(this, + R.string.no_such_item, Toast.LENGTH_LONG).show(); + finish(); + return; + } + if (contentType.startsWith( + ContentResolver.CURSOR_DIR_BASE_TYPE)) { + int mediaType = intent.getIntExtra(KEY_MEDIA_TYPES, 0); + if (mediaType != 0) { + uri = uri.buildUpon().appendQueryParameter( + KEY_MEDIA_TYPES, String.valueOf(mediaType)) + .build(); + } + Path albumPath = dm.findPathByUri(uri); + if (albumPath != null) { + MediaSet mediaSet = (MediaSet) dm.getMediaObject(albumPath); + data.putString(AlbumPage.KEY_MEDIA_PATH, albumPath.toString()); + getStateManager().startState(AlbumPage.class, data); + } else { + startDefaultPage(); + } + } else { + Path itemPath = dm.findPathByUri(uri); + Path albumPath = dm.getDefaultSetOf(itemPath); + if (albumPath != null) { + data.putString(PhotoPage.KEY_MEDIA_SET_PATH, + albumPath.toString()); + } + data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, itemPath.toString()); + getStateManager().startState(PhotoPage.class, data); + } + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + return getStateManager().createOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + GLRoot root = getGLRoot(); + root.lockRenderThread(); + try { + return getStateManager().itemSelected(item); + } finally { + root.unlockRenderThread(); + } + } + + @Override + public void onBackPressed() { + // send the back event to the top sub-state + GLRoot root = getGLRoot(); + root.lockRenderThread(); + try { + getStateManager().onBackPressed(); + } finally { + root.unlockRenderThread(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + GLRoot root = getGLRoot(); + root.lockRenderThread(); + try { + getStateManager().destroy(); + } finally { + root.unlockRenderThread(); + } + } + + @Override + protected void onResume() { + Utils.assertTrue(getStateManager().getStateCount() > 0); + super.onResume(); + } + + @Override + public GalleryActionBar getGalleryActionBar() { + return mActionBar; + } +} diff --git a/src/com/android/gallery3d/app/GalleryActionBar.java b/src/com/android/gallery3d/app/GalleryActionBar.java new file mode 100644 index 000000000..b9b59ee39 --- /dev/null +++ b/src/com/android/gallery3d/app/GalleryActionBar.java @@ -0,0 +1,218 @@ +/* + * 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.app; + +import java.util.ArrayList; + +import com.android.gallery3d.R; + +import android.app.ActionBar; +import android.app.ActionBar.Tab; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.FragmentTransaction; +import android.content.Context; +import android.content.DialogInterface; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.ShareActionProvider; + +public class GalleryActionBar implements ActionBar.TabListener { + private static final String TAG = "GalleryActionBar"; + + public interface ClusterRunner { + public void doCluster(int id); + } + + private static class ActionItem { + public int action; + public boolean enabled; + public boolean visible; + public int tabTitle; + public int dialogTitle; + public int clusterBy; + + public ActionItem(int action, boolean applied, boolean enabled, int title, + int clusterBy) { + this(action, applied, enabled, title, title, clusterBy); + } + + public ActionItem(int action, boolean applied, boolean enabled, int tabTitle, + int dialogTitle, int clusterBy) { + this.action = action; + this.enabled = enabled; + this.tabTitle = tabTitle; + this.dialogTitle = dialogTitle; + this.clusterBy = clusterBy; + this.visible = true; + } + } + + private static final ActionItem[] sClusterItems = new ActionItem[] { + new ActionItem(FilterUtils.CLUSTER_BY_ALBUM, true, false, R.string.albums, + R.string.group_by_album), + new ActionItem(FilterUtils.CLUSTER_BY_LOCATION, true, false, + R.string.locations, R.string.location, R.string.group_by_location), + new ActionItem(FilterUtils.CLUSTER_BY_TIME, true, false, R.string.times, + R.string.time, R.string.group_by_time), + new ActionItem(FilterUtils.CLUSTER_BY_FACE, true, false, R.string.people, + R.string.group_by_faces), + new ActionItem(FilterUtils.CLUSTER_BY_TAG, true, false, R.string.tags, + R.string.group_by_tags) + }; + + private ClusterRunner mClusterRunner; + private CharSequence[] mTitles; + private ArrayList<Integer> mActions; + private Context mContext; + private ActionBar mActionBar; + // We need this because ActionBar.getSelectedTab() doesn't work when + // ActionBar is hidden. + private Tab mCurrentTab; + + public GalleryActionBar(Activity activity) { + mActionBar = activity.getActionBar(); + mContext = activity; + + for (ActionItem item : sClusterItems) { + mActionBar.addTab(mActionBar.newTab().setText(item.tabTitle). + setTag(item).setTabListener(this)); + } + } + + public static int getHeight(Activity activity) { + ActionBar actionBar = activity.getActionBar(); + return actionBar != null ? actionBar.getHeight() : 0; + } + + private void createDialogData() { + ArrayList<CharSequence> titles = new ArrayList<CharSequence>(); + mActions = new ArrayList<Integer>(); + for (ActionItem item : sClusterItems) { + if (item.enabled && item.visible) { + titles.add(mContext.getString(item.dialogTitle)); + mActions.add(item.action); + } + } + mTitles = new CharSequence[titles.size()]; + titles.toArray(mTitles); + } + + public void setClusterItemEnabled(int id, boolean enabled) { + for (ActionItem item : sClusterItems) { + if (item.action == id) { + item.enabled = enabled; + return; + } + } + } + + public void setClusterItemVisibility(int id, boolean visible) { + for (ActionItem item : sClusterItems) { + if (item.action == id) { + item.visible = visible; + return; + } + } + } + + public int getClusterTypeAction() { + if (mCurrentTab != null) { + ActionItem item = (ActionItem) mCurrentTab.getTag(); + return item.action; + } + // By default, it's group-by-album + return FilterUtils.CLUSTER_BY_ALBUM; + } + + public static String getClusterByTypeString(Context context, int type) { + for (ActionItem item : sClusterItems) { + if (item.action == type) { + return context.getString(item.clusterBy); + } + } + return null; + } + + public static ShareActionProvider initializeShareActionProvider(Menu menu) { + MenuItem item = menu.findItem(R.id.action_share); + ShareActionProvider shareActionProvider = null; + if (item != null) { + shareActionProvider = (ShareActionProvider) item.getActionProvider(); + shareActionProvider.setShareHistoryFileName( + ShareActionProvider.DEFAULT_SHARE_HISTORY_FILE_NAME); + } + return shareActionProvider; + } + + public void showClusterTabs(ClusterRunner runner) { + mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); + mClusterRunner = runner; + } + + public void hideClusterTabs() { + mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); + mClusterRunner = null; + } + + public void showClusterDialog(final ClusterRunner clusterRunner) { + createDialogData(); + final ArrayList<Integer> actions = mActions; + new AlertDialog.Builder(mContext).setTitle(R.string.group_by).setItems( + mTitles, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + clusterRunner.doCluster(actions.get(which).intValue()); + } + }).create().show(); + } + + public void setTitle(String title) { + if (mActionBar != null) mActionBar.setTitle(title); + } + + public void setTitle(int titleId) { + if (mActionBar != null) mActionBar.setTitle(titleId); + } + + public void setSubtitle(String title) { + if (mActionBar != null) mActionBar.setSubtitle(title); + } + + public void setNavigationMode(int mode) { + if (mActionBar != null) mActionBar.setNavigationMode(mode); + } + + public int getHeight() { + return mActionBar == null ? 0 : mActionBar.getHeight(); + } + + @Override + public void onTabSelected(Tab tab, FragmentTransaction ft) { + if (mCurrentTab == tab) return; + mCurrentTab = tab; + ActionItem item = (ActionItem) tab.getTag(); + if (mClusterRunner != null) mClusterRunner.doCluster(item.action); + } + + @Override + public void onTabUnselected(Tab tab, FragmentTransaction ft) { + } + + @Override + public void onTabReselected(Tab tab, FragmentTransaction ft) { + } +} diff --git a/src/com/android/gallery3d/app/GalleryActivity.java b/src/com/android/gallery3d/app/GalleryActivity.java new file mode 100644 index 000000000..02f2f72f3 --- /dev/null +++ b/src/com/android/gallery3d/app/GalleryActivity.java @@ -0,0 +1,28 @@ +/* + * 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.app; + +import com.android.gallery3d.ui.GLRoot; +import com.android.gallery3d.ui.PositionRepository; + +public interface GalleryActivity extends GalleryContext { + public StateManager getStateManager(); + public GLRoot getGLRoot(); + public PositionRepository getPositionRepository(); + public GalleryApp getGalleryApplication(); + public GalleryActionBar getGalleryActionBar(); +} diff --git a/src/com/android/gallery3d/app/GalleryApp.java b/src/com/android/gallery3d/app/GalleryApp.java new file mode 100644 index 000000000..b3a305e53 --- /dev/null +++ b/src/com/android/gallery3d/app/GalleryApp.java @@ -0,0 +1,39 @@ +/* + * 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.app; + +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.DownloadCache; +import com.android.gallery3d.data.ImageCacheService; +import com.android.gallery3d.util.ThreadPool; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.os.Looper; + +public interface GalleryApp { + public DataManager getDataManager(); + public ImageCacheService getImageCacheService(); + public DownloadCache getDownloadCache(); + public ThreadPool getThreadPool(); + + public Context getAndroidContext(); + public Looper getMainLooper(); + public ContentResolver getContentResolver(); + public Resources getResources(); +} diff --git a/src/com/android/gallery3d/app/GalleryAppImpl.java b/src/com/android/gallery3d/app/GalleryAppImpl.java new file mode 100644 index 000000000..a11d92017 --- /dev/null +++ b/src/com/android/gallery3d/app/GalleryAppImpl.java @@ -0,0 +1,90 @@ +/* + * 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.app; + +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.DownloadCache; +import com.android.gallery3d.data.ImageCacheService; +import com.android.gallery3d.picasasource.PicasaSource; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.ThreadPool; +import com.android.gallery3d.widget.WidgetUtils; + +import android.app.Application; +import android.content.Context; + +import java.io.File; + +public class GalleryAppImpl extends Application implements GalleryApp { + + private static final String DOWNLOAD_FOLDER = "download"; + private static final long DOWNLOAD_CAPACITY = 64 * 1024 * 1024; // 64M + + private ImageCacheService mImageCacheService; + private DataManager mDataManager; + private ThreadPool mThreadPool; + private DownloadCache mDownloadCache; + + @Override + public void onCreate() { + super.onCreate(); + GalleryUtils.initialize(this); + WidgetUtils.initialize(this); + PicasaSource.initialize(this); + } + + public Context getAndroidContext() { + return this; + } + + public synchronized DataManager getDataManager() { + if (mDataManager == null) { + mDataManager = new DataManager(this); + mDataManager.initializeSourceMap(); + } + return mDataManager; + } + + public synchronized ImageCacheService getImageCacheService() { + if (mImageCacheService == null) { + mImageCacheService = new ImageCacheService(getAndroidContext()); + } + return mImageCacheService; + } + + public synchronized ThreadPool getThreadPool() { + if (mThreadPool == null) { + mThreadPool = new ThreadPool(); + } + return mThreadPool; + } + + public synchronized DownloadCache getDownloadCache() { + if (mDownloadCache == null) { + File cacheDir = new File(getExternalCacheDir(), DOWNLOAD_FOLDER); + + if (!cacheDir.isDirectory()) cacheDir.mkdirs(); + + if (!cacheDir.isDirectory()) { + throw new RuntimeException( + "fail to create: " + cacheDir.getAbsolutePath()); + } + mDownloadCache = new DownloadCache(this, cacheDir, DOWNLOAD_CAPACITY); + } + return mDownloadCache; + } +} diff --git a/src/com/android/gallery3d/app/GalleryContext.java b/src/com/android/gallery3d/app/GalleryContext.java new file mode 100644 index 000000000..022b4a704 --- /dev/null +++ b/src/com/android/gallery3d/app/GalleryContext.java @@ -0,0 +1,38 @@ +/* + * 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.app; + +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.ImageCacheService; +import com.android.gallery3d.util.ThreadPool; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.os.Looper; + +public interface GalleryContext { + public ImageCacheService getImageCacheService(); + public DataManager getDataManager(); + + public Context getAndroidContext(); + + public Looper getMainLooper(); + public Resources getResources(); + public ContentResolver getContentResolver(); + public ThreadPool getThreadPool(); +} diff --git a/src/com/android/gallery3d/app/LoadingListener.java b/src/com/android/gallery3d/app/LoadingListener.java new file mode 100644 index 000000000..ecbd798d2 --- /dev/null +++ b/src/com/android/gallery3d/app/LoadingListener.java @@ -0,0 +1,22 @@ +/* + * 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.app; + +public interface LoadingListener { + public void onLoadingStarted(); + public void onLoadingFinished(); +} diff --git a/src/com/android/gallery3d/app/Log.java b/src/com/android/gallery3d/app/Log.java new file mode 100644 index 000000000..07a8ea588 --- /dev/null +++ b/src/com/android/gallery3d/app/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.app; + +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/app/ManageCachePage.java b/src/com/android/gallery3d/app/ManageCachePage.java new file mode 100644 index 000000000..a0190db77 --- /dev/null +++ b/src/com/android/gallery3d/app/ManageCachePage.java @@ -0,0 +1,271 @@ +/* + * 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.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.AlbumSetView; +import com.android.gallery3d.ui.CacheBarView; +import com.android.gallery3d.ui.GLCanvas; +import com.android.gallery3d.ui.GLView; +import com.android.gallery3d.ui.ManageCacheDrawer; +import com.android.gallery3d.ui.MenuExecutor; +import com.android.gallery3d.ui.SelectionDrawer; +import com.android.gallery3d.ui.SelectionManager; +import com.android.gallery3d.ui.SlotView; +import com.android.gallery3d.ui.StaticBackground; +import com.android.gallery3d.util.GalleryUtils; + +import android.app.ActionBar; +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.widget.Toast; + +import java.util.ArrayList; + +public class ManageCachePage extends ActivityState implements + SelectionManager.SelectionListener, CacheBarView.Listener, + MenuExecutor.ProgressListener, EyePosition.EyePositionListener { + public static final String KEY_MEDIA_PATH = "media-path"; + private static final String TAG = "ManageCachePage"; + + private static final float USER_DISTANCE_METER = 0.3f; + private static final int DATA_CACHE_SIZE = 256; + + private StaticBackground mStaticBackground; + private AlbumSetView mAlbumSetView; + + private MediaSet mMediaSet; + + protected SelectionManager mSelectionManager; + protected SelectionDrawer mSelectionDrawer; + private AlbumSetDataAdapter mAlbumSetDataAdapter; + private float mUserDistance; // in pixel + + private CacheBarView mCacheBar; + + private EyePosition mEyePosition; + + // The eyes' position of the user, the origin is at the center of the + // device and the unit is in pixels. + private float mX; + private float mY; + private float mZ; + + private int mAlbumCountToMakeAvailableOffline; + + private GLView mRootPane = new GLView() { + private float mMatrix[] = new float[16]; + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + mStaticBackground.layout(0, 0, right - left, bottom - top); + mEyePosition.resetPosition(); + + Config.ManageCachePage config = Config.ManageCachePage.get((Context) mActivity); + + ActionBar actionBar = ((Activity) mActivity).getActionBar(); + int slotViewTop = GalleryActionBar.getHeight((Activity) mActivity); + int slotViewBottom = bottom - top - config.cacheBarHeight; + + mAlbumSetView.layout(0, slotViewTop, right - left, slotViewBottom); + mCacheBar.layout(0, bottom - top - config.cacheBarHeight, + right - left, bottom - top); + } + + @Override + protected void render(GLCanvas canvas) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + GalleryUtils.setViewPointMatrix(mMatrix, + getWidth() / 2 + mX, getHeight() / 2 + mY, mZ); + canvas.multiplyMatrix(mMatrix, 0); + super.render(canvas); + canvas.restore(); + } + }; + + public void onEyePositionChanged(float x, float y, float z) { + mRootPane.lockRendering(); + mX = x; + mY = y; + mZ = z; + mRootPane.unlockRendering(); + mRootPane.invalidate(); + } + + public void onSingleTapUp(int slotIndex) { + MediaSet targetSet = mAlbumSetDataAdapter.getMediaSet(slotIndex); + if (targetSet == null) return; // Content is dirty, we shall reload soon + + // ignore selection action if the target set does not support cache + // operation (like a local album). + if ((targetSet.getSupportedOperations() + & MediaSet.SUPPORT_CACHE) == 0) { + showToastForLocalAlbum(); + return; + } + + Path path = targetSet.getPath(); + boolean isFullyCached = + (targetSet.getCacheFlag() == MediaObject.CACHE_FLAG_FULL); + boolean isSelected = mSelectionManager.isItemSelected(path); + + if (!isFullyCached) { + // We only count the media sets that will be made available offline + // in this session. + if (isSelected) { + --mAlbumCountToMakeAvailableOffline; + } else { + ++mAlbumCountToMakeAvailableOffline; + } + } + + long sizeOfTarget = targetSet.getCacheSize(); + if (isFullyCached ^ isSelected) { + mCacheBar.increaseTargetCacheSize(-sizeOfTarget); + } else { + mCacheBar.increaseTargetCacheSize(sizeOfTarget); + } + + mSelectionManager.toggle(path); + mAlbumSetView.invalidate(); + } + + @Override + public void onCreate(Bundle data, Bundle restoreState) { + initializeViews(); + initializeData(data); + mEyePosition = new EyePosition(mActivity.getAndroidContext(), this); + } + + @Override + public void onPause() { + super.onPause(); + mAlbumSetDataAdapter.pause(); + mAlbumSetView.pause(); + mCacheBar.pause(); + mEyePosition.pause(); + } + + @Override + public void onResume() { + super.onResume(); + setContentPane(mRootPane); + mAlbumSetDataAdapter.resume(); + mAlbumSetView.resume(); + mCacheBar.resume(); + mEyePosition.resume(); + } + + private void initializeData(Bundle data) { + mUserDistance = GalleryUtils.meterToPixel(USER_DISTANCE_METER); + String mediaPath = data.getString(ManageCachePage.KEY_MEDIA_PATH); + mMediaSet = mActivity.getDataManager().getMediaSet(mediaPath); + mSelectionManager.setSourceMediaSet(mMediaSet); + + // We will always be in selection mode in this page. + mSelectionManager.setAutoLeaveSelectionMode(false); + mSelectionManager.enterSelectionMode(); + + mAlbumSetDataAdapter = new AlbumSetDataAdapter( + mActivity, mMediaSet, DATA_CACHE_SIZE); + mAlbumSetView.setModel(mAlbumSetDataAdapter); + } + + private void initializeViews() { + mSelectionManager = new SelectionManager(mActivity, true); + mSelectionManager.setSelectionListener(this); + mStaticBackground = new StaticBackground(mActivity.getAndroidContext()); + mRootPane.addComponent(mStaticBackground); + + mSelectionDrawer = new ManageCacheDrawer( + (Context) mActivity, mSelectionManager); + Config.ManageCachePage config = Config.ManageCachePage.get((Context) mActivity); + mAlbumSetView = new AlbumSetView(mActivity, mSelectionDrawer, + config.slotWidth, config.slotHeight, + config.displayItemSize, config.labelFontSize, + config.labelOffsetY, config.labelMargin); + mAlbumSetView.setListener(new SlotView.SimpleListener() { + @Override + public void onSingleTapUp(int slotIndex) { + ManageCachePage.this.onSingleTapUp(slotIndex); + } + }); + mRootPane.addComponent(mAlbumSetView); + + mCacheBar = new CacheBarView(mActivity, R.drawable.manage_bar, + config.cacheBarHeight, + config.cacheBarPinLeftMargin, + config.cacheBarPinRightMargin, + config.cacheBarButtonRightMargin, + config.cacheBarFontSize); + + mCacheBar.setListener(this); + mRootPane.addComponent(mCacheBar); + + mStaticBackground.setImage(R.drawable.background, + R.drawable.background_portrait); + } + + public void onDoneClicked() { + ArrayList<Path> ids = mSelectionManager.getSelected(false); + if (ids.size() == 0) { + onBackPressed(); + return; + } + showToast(); + + MenuExecutor menuExecutor = new MenuExecutor(mActivity, + mSelectionManager); + menuExecutor.startAction(R.id.action_toggle_full_caching, + R.string.process_caching_requests, this); + } + + private void showToast() { + if (mAlbumCountToMakeAvailableOffline > 0) { + Activity activity = (Activity) mActivity; + Toast.makeText(activity, activity.getResources().getQuantityString( + R.plurals.make_albums_available_offline, + mAlbumCountToMakeAvailableOffline), + Toast.LENGTH_SHORT).show(); + } + } + + private void showToastForLocalAlbum() { + Activity activity = (Activity) mActivity; + Toast.makeText(activity, activity.getResources().getString( + R.string.try_to_set_local_album_available_offline), + Toast.LENGTH_SHORT).show(); + } + + public void onProgressComplete(int result) { + onBackPressed(); + } + + public void onProgressUpdate(int index) { + } + + public void onSelectionModeChange(int mode) { + } + + public void onSelectionChange(Path path, boolean selected) { + } +} diff --git a/src/com/android/gallery3d/app/MovieActivity.java b/src/com/android/gallery3d/app/MovieActivity.java new file mode 100644 index 000000000..fea364e85 --- /dev/null +++ b/src/com/android/gallery3d/app/MovieActivity.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2007 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.app; + +import com.android.gallery3d.R; + +import android.app.Activity; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.database.Cursor; +import android.media.AudioManager; +import android.os.Bundle; +import android.provider.MediaStore; +import android.provider.MediaStore.Video.VideoColumns; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +/** + * This activity plays a video from a specified URI. + */ +public class MovieActivity extends Activity { + @SuppressWarnings("unused") + private static final String TAG = "MovieActivity"; + + private MoviePlayer mPlayer; + private boolean mFinishOnCompletion; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + requestWindowFeature(Window.FEATURE_ACTION_BAR); + requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); + + setContentView(R.layout.movie_view); + View rootView = findViewById(R.id.root); + Intent intent = getIntent(); + setVideoTitle(intent); + mPlayer = new MoviePlayer(rootView, this, intent.getData()) { + @Override + public void onCompletion() { + if (mFinishOnCompletion) { + finish(); + } + } + }; + if (intent.hasExtra(MediaStore.EXTRA_SCREEN_ORIENTATION)) { + int orientation = intent.getIntExtra( + MediaStore.EXTRA_SCREEN_ORIENTATION, + ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + if (orientation != getRequestedOrientation()) { + setRequestedOrientation(orientation); + } + } + mFinishOnCompletion = intent.getBooleanExtra(MediaStore.EXTRA_FINISH_ON_COMPLETION, true); + Window win = getWindow(); + WindowManager.LayoutParams winParams = win.getAttributes(); + winParams.buttonBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF; + win.setAttributes(winParams); + + } + + private void setVideoTitle(Intent intent) { + String title = intent.getStringExtra(Intent.EXTRA_TITLE); + if (title == null) { + Cursor cursor = null; + try { + cursor = getContentResolver().query(intent.getData(), + new String[] {VideoColumns.TITLE}, null, null, null); + if (cursor != null && cursor.moveToNext()) { + title = cursor.getString(0); + } + } catch (Throwable t) { + Log.w(TAG, "cannot get title from: " + intent.getDataString(), t); + } finally { + if (cursor != null) cursor.close(); + } + } + if (title != null) getActionBar().setTitle(title); + } + + @Override + public void onStart() { + ((AudioManager) getSystemService(AUDIO_SERVICE)) + .requestAudioFocus(null, AudioManager.STREAM_MUSIC, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + super.onStart(); + } + + @Override + protected void onStop() { + ((AudioManager) getSystemService(AUDIO_SERVICE)) + .abandonAudioFocus(null); + super.onStop(); + } + + @Override + public void onPause() { + mPlayer.onPause(); + super.onPause(); + } + + @Override + public void onResume() { + mPlayer.onResume(); + super.onResume(); + } + + @Override + public void onDestroy() { + mPlayer.onDestroy(); + super.onDestroy(); + } +} diff --git a/src/com/android/gallery3d/app/MoviePlayer.java b/src/com/android/gallery3d/app/MoviePlayer.java new file mode 100644 index 000000000..423994485 --- /dev/null +++ b/src/com/android/gallery3d/app/MoviePlayer.java @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2009 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.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.BlobCache; +import com.android.gallery3d.util.CacheManager; +import com.android.gallery3d.util.GalleryUtils; + +import android.app.ActionBar; +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.net.Uri; +import android.os.Handler; +import android.view.KeyEvent; +import android.view.View; +import android.widget.MediaController; +import android.widget.VideoView; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +public class MoviePlayer implements + MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener { + @SuppressWarnings("unused") + private static final String TAG = "MoviePlayer"; + + // Copied from MediaPlaybackService in the Music Player app. + private static final String SERVICECMD = "com.android.music.musicservicecommand"; + private static final String CMDNAME = "command"; + private static final String CMDPAUSE = "pause"; + + private Context mContext; + private final VideoView mVideoView; + private final View mProgressView; + private final Bookmarker mBookmarker; + private final Uri mUri; + private final Handler mHandler = new Handler(); + private final AudioBecomingNoisyReceiver mAudioBecomingNoisyReceiver; + private final ActionBar mActionBar; + + private boolean mHasPaused; + + private final Runnable mPlayingChecker = new Runnable() { + public void run() { + if (mVideoView.isPlaying()) { + mProgressView.setVisibility(View.GONE); + } else { + mHandler.postDelayed(mPlayingChecker, 250); + } + } + }; + + public MoviePlayer(View rootView, final MovieActivity movieActivity, Uri videoUri) { + mContext = movieActivity.getApplicationContext(); + mVideoView = (VideoView) rootView.findViewById(R.id.surface_view); + mProgressView = rootView.findViewById(R.id.progress_indicator); + mBookmarker = new Bookmarker(movieActivity); + mActionBar = movieActivity.getActionBar(); + mUri = videoUri; + + // For streams that we expect to be slow to start up, show a + // progress spinner until playback starts. + String scheme = mUri.getScheme(); + if ("http".equalsIgnoreCase(scheme) || "rtsp".equalsIgnoreCase(scheme)) { + mHandler.postDelayed(mPlayingChecker, 250); + } else { + mProgressView.setVisibility(View.GONE); + } + + mVideoView.setOnErrorListener(this); + mVideoView.setOnCompletionListener(this); + mVideoView.setVideoURI(mUri); + + MediaController mediaController = new MediaController(movieActivity) { + @Override + public void show() { + super.show(); + mActionBar.show(); + } + + @Override + public void hide() { + super.hide(); + mActionBar.hide(); + } + }; + mVideoView.setMediaController(mediaController); + mediaController.setOnKeyListener(new View.OnKeyListener() { + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (event.getAction() == KeyEvent.ACTION_UP) { + movieActivity.onBackPressed(); + } + return true; + } + return false; + } + }); + + mAudioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(); + mAudioBecomingNoisyReceiver.register(); + + // make the video view handle keys for seeking and pausing + mVideoView.requestFocus(); + + Intent i = new Intent(SERVICECMD); + i.putExtra(CMDNAME, CMDPAUSE); + movieActivity.sendBroadcast(i); + + final Integer bookmark = mBookmarker.getBookmark(mUri); + if (bookmark != null) { + showResumeDialog(movieActivity, bookmark); + } else { + mVideoView.start(); + } + } + + private void showResumeDialog(Context context, final int bookmark) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.resume_playing_title); + builder.setMessage(String.format( + context.getString(R.string.resume_playing_message), + GalleryUtils.formatDuration(context, bookmark / 1000))); + builder.setOnCancelListener(new OnCancelListener() { + public void onCancel(DialogInterface dialog) { + onCompletion(); + } + }); + builder.setPositiveButton( + R.string.resume_playing_resume, new OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + mVideoView.seekTo(bookmark); + mVideoView.start(); + } + }); + builder.setNegativeButton( + R.string.resume_playing_restart, new OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + mVideoView.start(); + } + }); + builder.show(); + } + + public void onPause() { + mHandler.removeCallbacksAndMessages(null); + mBookmarker.setBookmark(mUri, mVideoView.getCurrentPosition(), + mVideoView.getDuration()); + mVideoView.suspend(); + mHasPaused = true; + } + + public void onResume() { + if (mHasPaused) { + Integer bookmark = mBookmarker.getBookmark(mUri); + if (bookmark != null) { + mVideoView.seekTo(bookmark); + } + } + mVideoView.resume(); + } + + public void onDestroy() { + mVideoView.stopPlayback(); + mAudioBecomingNoisyReceiver.unregister(); + } + + public boolean onError(MediaPlayer player, int arg1, int arg2) { + mHandler.removeCallbacksAndMessages(null); + mProgressView.setVisibility(View.GONE); + return false; + } + + public void onCompletion(MediaPlayer mp) { + onCompletion(); + } + + public void onCompletion() { + } + + private class AudioBecomingNoisyReceiver extends BroadcastReceiver { + + public void register() { + mContext.registerReceiver(this, + new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); + } + + public void unregister() { + mContext.unregisterReceiver(this); + } + + @Override + public void onReceive(Context context, Intent intent) { + if (mVideoView.isPlaying()) { + mVideoView.pause(); + } + } + } +} + +class Bookmarker { + private static final String TAG = "Bookmarker"; + + private static final String BOOKMARK_CACHE_FILE = "bookmark"; + private static final int BOOKMARK_CACHE_MAX_ENTRIES = 100; + private static final int BOOKMARK_CACHE_MAX_BYTES = 10 * 1024; + private static final int BOOKMARK_CACHE_VERSION = 1; + + private static final int HALF_MINUTE = 30 * 1000; + private static final int TWO_MINUTES = 4 * HALF_MINUTE; + + private final Context mContext; + + public Bookmarker(Context context) { + mContext = context; + } + + public void setBookmark(Uri uri, int bookmark, int duration) { + try { + BlobCache cache = CacheManager.getCache(mContext, + BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES, + BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeUTF(uri.toString()); + dos.writeInt(bookmark); + dos.writeInt(duration); + dos.flush(); + cache.insert(uri.hashCode(), bos.toByteArray()); + } catch (Throwable t) { + Log.w(TAG, "setBookmark failed", t); + } + } + + public Integer getBookmark(Uri uri) { + try { + BlobCache cache = CacheManager.getCache(mContext, + BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES, + BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION); + + byte[] data = cache.lookup(uri.hashCode()); + if (data == null) return null; + + DataInputStream dis = new DataInputStream( + new ByteArrayInputStream(data)); + + String uriString = dis.readUTF(dis); + int bookmark = dis.readInt(); + int duration = dis.readInt(); + + if (!uriString.equals(uri.toString())) { + return null; + } + + if ((bookmark < HALF_MINUTE) || (duration < TWO_MINUTES) + || (bookmark > (duration - HALF_MINUTE))) { + return null; + } + return Integer.valueOf(bookmark); + } catch (Throwable t) { + Log.w(TAG, "getBookmark failed", t); + } + return null; + } +} diff --git a/src/com/android/gallery3d/app/PackagesMonitor.java b/src/com/android/gallery3d/app/PackagesMonitor.java new file mode 100644 index 000000000..cb202a31c --- /dev/null +++ b/src/com/android/gallery3d/app/PackagesMonitor.java @@ -0,0 +1,50 @@ +/* + * 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.app; + +import com.android.gallery3d.picasasource.PicasaSource; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +public class PackagesMonitor extends BroadcastReceiver { + public static final String KEY_PACKAGES_VERSION = "packages-version"; + + public synchronized static int getPackagesVersion(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + return prefs.getInt(KEY_PACKAGES_VERSION, 1); + } + + @Override + public void onReceive(Context context, Intent intent) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + int version = prefs.getInt(KEY_PACKAGES_VERSION, 1); + prefs.edit().putInt(KEY_PACKAGES_VERSION, version + 1).commit(); + + String action = intent.getAction(); + String packageName = intent.getData().getSchemeSpecificPart(); + if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { + PicasaSource.onPackageAdded(context, packageName); + } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { + PicasaSource.onPackageRemoved(context, packageName); + } + } +} diff --git a/src/com/android/gallery3d/app/PhotoDataAdapter.java b/src/com/android/gallery3d/app/PhotoDataAdapter.java new file mode 100644 index 000000000..c05c89a0d --- /dev/null +++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java @@ -0,0 +1,794 @@ +/* + * 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.app; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.PhotoView; +import com.android.gallery3d.ui.PhotoView.ImageData; +import com.android.gallery3d.ui.SynchronizedHandler; +import com.android.gallery3d.ui.TileImageViewAdapter; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.ThreadPool; + +import android.graphics.Bitmap; +import android.graphics.BitmapRegionDecoder; +import android.os.Handler; +import android.os.Message; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +public class PhotoDataAdapter implements PhotoPage.Model { + @SuppressWarnings("unused") + private static final String TAG = "PhotoDataAdapter"; + + private static final int MSG_LOAD_START = 1; + private static final int MSG_LOAD_FINISH = 2; + private static final int MSG_RUN_OBJECT = 3; + + private static final int MIN_LOAD_COUNT = 8; + private static final int DATA_CACHE_SIZE = 32; + private static final int IMAGE_CACHE_SIZE = 5; + + private static final int BIT_SCREEN_NAIL = 1; + private static final int BIT_FULL_IMAGE = 2; + + private static final long VERSION_OUT_OF_RANGE = MediaObject.nextVersionNumber(); + + // sImageFetchSeq is the fetching sequence for images. + // We want to fetch the current screennail first (offset = 0), the next + // screennail (offset = +1), then the previous screennail (offset = -1) etc. + // After all the screennail are fetched, we fetch the full images (only some + // of them because of we don't want to use too much memory). + private static ImageFetch[] sImageFetchSeq; + + private static class ImageFetch { + int indexOffset; + int imageBit; + public ImageFetch(int offset, int bit) { + indexOffset = offset; + imageBit = bit; + } + } + + static { + int k = 0; + sImageFetchSeq = new ImageFetch[1 + (IMAGE_CACHE_SIZE - 1) * 2 + 3]; + sImageFetchSeq[k++] = new ImageFetch(0, BIT_SCREEN_NAIL); + + for (int i = 1; i < IMAGE_CACHE_SIZE; ++i) { + sImageFetchSeq[k++] = new ImageFetch(i, BIT_SCREEN_NAIL); + sImageFetchSeq[k++] = new ImageFetch(-i, BIT_SCREEN_NAIL); + } + + sImageFetchSeq[k++] = new ImageFetch(0, BIT_FULL_IMAGE); + sImageFetchSeq[k++] = new ImageFetch(1, BIT_FULL_IMAGE); + sImageFetchSeq[k++] = new ImageFetch(-1, BIT_FULL_IMAGE); + } + + private final TileImageViewAdapter mTileProvider = new TileImageViewAdapter(); + + // PhotoDataAdapter caches MediaItems (data) and ImageEntries (image). + // + // The MediaItems are stored in the mData array, which has DATA_CACHE_SIZE + // entries. The valid index range are [mContentStart, mContentEnd). We keep + // mContentEnd - mContentStart <= DATA_CACHE_SIZE, so we can use + // (i % DATA_CACHE_SIZE) as index to the array. + // + // The valid MediaItem window size (mContentEnd - mContentStart) may be + // smaller than DATA_CACHE_SIZE because we only update the window and reload + // the MediaItems when there are significant changes to the window position + // (>= MIN_LOAD_COUNT). + private final MediaItem mData[] = new MediaItem[DATA_CACHE_SIZE]; + private int mContentStart = 0; + private int mContentEnd = 0; + + /* + * The ImageCache is a version-to-ImageEntry map. It only holds + * the ImageEntries in the range of [mActiveStart, mActiveEnd). + * We also keep mActiveEnd - mActiveStart <= IMAGE_CACHE_SIZE. + * Besides, the [mActiveStart, mActiveEnd) range must be contained + * within the[mContentStart, mContentEnd) range. + */ + private HashMap<Long, ImageEntry> mImageCache = new HashMap<Long, ImageEntry>(); + private int mActiveStart = 0; + private int mActiveEnd = 0; + + // mCurrentIndex is the "center" image the user is viewing. The change of + // mCurrentIndex triggers the data loading and image loading. + private int mCurrentIndex; + + // mChanges keeps the version number (of MediaItem) about the previous, + // current, and next image. If the version number changes, we invalidate + // the model. This is used after a database reload or mCurrentIndex changes. + private final long mChanges[] = new long[3]; + + private final Handler mMainHandler; + private final ThreadPool mThreadPool; + + private final PhotoView mPhotoView; + private final MediaSet mSource; + private ReloadTask mReloadTask; + + private long mSourceVersion = MediaObject.INVALID_DATA_VERSION; + private int mSize = 0; + private Path mItemPath; + private boolean mIsActive; + + public interface DataListener extends LoadingListener { + public void onPhotoChanged(int index, Path item); + } + + private DataListener mDataListener; + + private final SourceListener mSourceListener = new SourceListener(); + + // The path of the current viewing item will be stored in mItemPath. + // If mItemPath is not null, mCurrentIndex is only a hint for where we + // can find the item. If mItemPath is null, then we use the mCurrentIndex to + // find the image being viewed. + public PhotoDataAdapter(GalleryActivity activity, + PhotoView view, MediaSet mediaSet, Path itemPath, int indexHint) { + mSource = Utils.checkNotNull(mediaSet); + mPhotoView = Utils.checkNotNull(view); + mItemPath = Utils.checkNotNull(itemPath); + mCurrentIndex = indexHint; + mThreadPool = activity.getThreadPool(); + + Arrays.fill(mChanges, MediaObject.INVALID_DATA_VERSION); + + mMainHandler = new SynchronizedHandler(activity.getGLRoot()) { + @SuppressWarnings("unchecked") + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_RUN_OBJECT: + ((Runnable) message.obj).run(); + return; + case MSG_LOAD_START: { + if (mDataListener != null) mDataListener.onLoadingStarted(); + return; + } + case MSG_LOAD_FINISH: { + if (mDataListener != null) mDataListener.onLoadingFinished(); + return; + } + default: throw new AssertionError(); + } + } + }; + + updateSlidingWindow(); + } + + private long getVersion(int index) { + if (index < 0 || index >= mSize) return VERSION_OUT_OF_RANGE; + if (index >= mContentStart && index < mContentEnd) { + MediaItem item = mData[index % DATA_CACHE_SIZE]; + if (item != null) return item.getDataVersion(); + } + return MediaObject.INVALID_DATA_VERSION; + } + + private void fireModelInvalidated() { + for (int i = -1; i <= 1; ++i) { + long current = getVersion(mCurrentIndex + i); + long change = mChanges[i + 1]; + if (current != change) { + mPhotoView.notifyImageInvalidated(i); + mChanges[i + 1] = current; + } + } + } + + public void setDataListener(DataListener listener) { + mDataListener = listener; + } + + private void updateScreenNail(long version, Future<Bitmap> future) { + ImageEntry entry = mImageCache.get(version); + if (entry == null || entry.screenNailTask == null) { + Bitmap screenNail = future.get(); + if (screenNail != null) screenNail.recycle(); + return; + } + entry.screenNailTask = null; + entry.screenNail = future.get(); + + if (entry.screenNail == null) { + entry.failToLoad = true; + } else { + for (int i = -1; i <=1; ++i) { + if (version == getVersion(mCurrentIndex + i)) { + if (i == 0) updateTileProvider(entry); + mPhotoView.notifyImageInvalidated(i); + } + } + } + updateImageRequests(); + } + + private void updateFullImage(long version, Future<BitmapRegionDecoder> future) { + ImageEntry entry = mImageCache.get(version); + if (entry == null || entry.fullImageTask == null) { + BitmapRegionDecoder fullImage = future.get(); + if (fullImage != null) fullImage.recycle(); + return; + } + entry.fullImageTask = null; + entry.fullImage = future.get(); + if (entry.fullImage != null) { + if (version == getVersion(mCurrentIndex)) { + updateTileProvider(entry); + mPhotoView.notifyImageInvalidated(0); + } + } + updateImageRequests(); + } + + public void resume() { + mIsActive = true; + mSource.addContentListener(mSourceListener); + updateImageCache(); + updateImageRequests(); + + mReloadTask = new ReloadTask(); + mReloadTask.start(); + + mPhotoView.notifyModelInvalidated(); + } + + public void pause() { + mIsActive = false; + + mReloadTask.terminate(); + mReloadTask = null; + + mSource.removeContentListener(mSourceListener); + + for (ImageEntry entry : mImageCache.values()) { + if (entry.fullImageTask != null) entry.fullImageTask.cancel(); + if (entry.screenNailTask != null) entry.screenNailTask.cancel(); + } + mImageCache.clear(); + mTileProvider.clear(); + } + + private ImageData getImage(int index) { + if (index < 0 || index >= mSize || !mIsActive) return null; + Utils.assertTrue(index >= mActiveStart && index < mActiveEnd); + + ImageEntry entry = mImageCache.get(getVersion(index)); + Bitmap screennail = entry == null ? null : entry.screenNail; + if (screennail != null) { + return new ImageData(screennail, entry.rotation); + } else { + return new ImageData(null, 0); + } + } + + public ImageData getPreviousImage() { + return getImage(mCurrentIndex - 1); + } + + public ImageData getNextImage() { + return getImage(mCurrentIndex + 1); + } + + private void updateCurrentIndex(int index) { + mCurrentIndex = index; + updateSlidingWindow(); + + MediaItem item = mData[index % DATA_CACHE_SIZE]; + mItemPath = item == null ? null : item.getPath(); + + updateImageCache(); + updateImageRequests(); + updateTileProvider(); + mPhotoView.notifyOnNewImage(); + + if (mDataListener != null) { + mDataListener.onPhotoChanged(index, mItemPath); + } + fireModelInvalidated(); + } + + public void next() { + updateCurrentIndex(mCurrentIndex + 1); + } + + public void previous() { + updateCurrentIndex(mCurrentIndex - 1); + } + + public void jumpTo(int index) { + if (mCurrentIndex == index) return; + updateCurrentIndex(index); + } + + public Bitmap getBackupImage() { + return mTileProvider.getBackupImage(); + } + + public int getImageHeight() { + return mTileProvider.getImageHeight(); + } + + public int getImageWidth() { + return mTileProvider.getImageWidth(); + } + + public int getImageRotation() { + ImageEntry entry = mImageCache.get(getVersion(mCurrentIndex)); + return entry == null ? 0 : entry.rotation; + } + + public int getLevelCount() { + return mTileProvider.getLevelCount(); + } + + public Bitmap getTile(int level, int x, int y, int tileSize) { + return mTileProvider.getTile(level, x, y, tileSize); + } + + public boolean isFailedToLoad() { + return mTileProvider.isFailedToLoad(); + } + + public boolean isEmpty() { + return mSize == 0; + } + + public int getCurrentIndex() { + return mCurrentIndex; + } + + public MediaItem getCurrentMediaItem() { + return mData[mCurrentIndex % DATA_CACHE_SIZE]; + } + + public void setCurrentPhoto(Path path, int indexHint) { + if (mItemPath == path) return; + mItemPath = path; + mCurrentIndex = indexHint; + updateSlidingWindow(); + updateImageCache(); + fireModelInvalidated(); + + // We need to reload content if the path doesn't match. + MediaItem item = getCurrentMediaItem(); + if (item != null && item.getPath() != path) { + if (mReloadTask != null) mReloadTask.notifyDirty(); + } + } + + private void updateTileProvider() { + ImageEntry entry = mImageCache.get(getVersion(mCurrentIndex)); + if (entry == null) { // in loading + mTileProvider.clear(); + } else { + updateTileProvider(entry); + } + } + + private void updateTileProvider(ImageEntry entry) { + Bitmap screenNail = entry.screenNail; + BitmapRegionDecoder fullImage = entry.fullImage; + if (screenNail != null) { + if (fullImage != null) { + mTileProvider.setBackupImage(screenNail, + fullImage.getWidth(), fullImage.getHeight()); + mTileProvider.setRegionDecoder(fullImage); + } else { + int width = screenNail.getWidth(); + int height = screenNail.getHeight(); + mTileProvider.setBackupImage(screenNail, width, height); + } + } else { + mTileProvider.clear(); + if (entry.failToLoad) mTileProvider.setFailedToLoad(); + } + } + + private void updateSlidingWindow() { + // 1. Update the image window + int start = Utils.clamp(mCurrentIndex - IMAGE_CACHE_SIZE / 2, + 0, Math.max(0, mSize - IMAGE_CACHE_SIZE)); + int end = Math.min(mSize, start + IMAGE_CACHE_SIZE); + + if (mActiveStart == start && mActiveEnd == end) return; + + mActiveStart = start; + mActiveEnd = end; + + // 2. Update the data window + start = Utils.clamp(mCurrentIndex - DATA_CACHE_SIZE / 2, + 0, Math.max(0, mSize - DATA_CACHE_SIZE)); + end = Math.min(mSize, start + DATA_CACHE_SIZE); + if (mContentStart > mActiveStart || mContentEnd < mActiveEnd + || Math.abs(start - mContentStart) > MIN_LOAD_COUNT) { + for (int i = mContentStart; i < mContentEnd; ++i) { + if (i < start || i >= end) { + mData[i % DATA_CACHE_SIZE] = null; + } + } + mContentStart = start; + mContentEnd = end; + if (mReloadTask != null) mReloadTask.notifyDirty(); + } + } + + private void updateImageRequests() { + if (!mIsActive) return; + + int currentIndex = mCurrentIndex; + MediaItem item = mData[currentIndex % DATA_CACHE_SIZE]; + if (item == null || item.getPath() != mItemPath) { + // current item mismatch - don't request image + return; + } + + // 1. Find the most wanted request and start it (if not already started). + Future<?> task = null; + for (int i = 0; i < sImageFetchSeq.length; i++) { + int offset = sImageFetchSeq[i].indexOffset; + int bit = sImageFetchSeq[i].imageBit; + task = startTaskIfNeeded(currentIndex + offset, bit); + if (task != null) break; + } + + // 2. Cancel everything else. + for (ImageEntry entry : mImageCache.values()) { + if (entry.screenNailTask != null && entry.screenNailTask != task) { + entry.screenNailTask.cancel(); + entry.screenNailTask = null; + entry.requestedBits &= ~BIT_SCREEN_NAIL; + } + if (entry.fullImageTask != null && entry.fullImageTask != task) { + entry.fullImageTask.cancel(); + entry.fullImageTask = null; + entry.requestedBits &= ~BIT_FULL_IMAGE; + } + } + } + + // Returns the task if we started the task or the task is already started. + private Future<?> startTaskIfNeeded(int index, int which) { + if (index < mActiveStart || index >= mActiveEnd) return null; + + ImageEntry entry = mImageCache.get(getVersion(index)); + if (entry == null) return null; + + if (which == BIT_SCREEN_NAIL && entry.screenNailTask != null) { + return entry.screenNailTask; + } else if (which == BIT_FULL_IMAGE && entry.fullImageTask != null) { + return entry.fullImageTask; + } + + MediaItem item = mData[index % DATA_CACHE_SIZE]; + Utils.assertTrue(item != null); + + if (which == BIT_SCREEN_NAIL + && (entry.requestedBits & BIT_SCREEN_NAIL) == 0) { + entry.requestedBits |= BIT_SCREEN_NAIL; + entry.screenNailTask = mThreadPool.submit( + item.requestImage(MediaItem.TYPE_THUMBNAIL), + new ScreenNailListener(item.getDataVersion())); + // request screen nail + return entry.screenNailTask; + } + if (which == BIT_FULL_IMAGE + && (entry.requestedBits & BIT_FULL_IMAGE) == 0 + && (item.getSupportedOperations() + & MediaItem.SUPPORT_FULL_IMAGE) != 0) { + entry.requestedBits |= BIT_FULL_IMAGE; + entry.fullImageTask = mThreadPool.submit( + item.requestLargeImage(), + new FullImageListener(item.getDataVersion())); + // request full image + return entry.fullImageTask; + } + return null; + } + + private void updateImageCache() { + HashSet<Long> toBeRemoved = new HashSet<Long>(mImageCache.keySet()); + for (int i = mActiveStart; i < mActiveEnd; ++i) { + MediaItem item = mData[i % DATA_CACHE_SIZE]; + long version = item == null + ? MediaObject.INVALID_DATA_VERSION + : item.getDataVersion(); + if (version == MediaObject.INVALID_DATA_VERSION) continue; + ImageEntry entry = mImageCache.get(version); + toBeRemoved.remove(version); + if (entry != null) { + if (Math.abs(i - mCurrentIndex) > 1) { + if (entry.fullImageTask != null) { + entry.fullImageTask.cancel(); + entry.fullImageTask = null; + } + entry.fullImage = null; + entry.requestedBits &= ~BIT_FULL_IMAGE; + } + } else { + entry = new ImageEntry(); + entry.rotation = item.getRotation(); + mImageCache.put(version, entry); + } + } + + // Clear the data and requests for ImageEntries outside the new window. + for (Long version : toBeRemoved) { + ImageEntry entry = mImageCache.remove(version); + if (entry.fullImageTask != null) entry.fullImageTask.cancel(); + if (entry.screenNailTask != null) entry.screenNailTask.cancel(); + } + } + + private class FullImageListener + implements Runnable, FutureListener<BitmapRegionDecoder> { + private final long mVersion; + private Future<BitmapRegionDecoder> mFuture; + + public FullImageListener(long version) { + mVersion = version; + } + + public void onFutureDone(Future<BitmapRegionDecoder> future) { + mFuture = future; + mMainHandler.sendMessage( + mMainHandler.obtainMessage(MSG_RUN_OBJECT, this)); + } + + public void run() { + updateFullImage(mVersion, mFuture); + } + } + + private class ScreenNailListener + implements Runnable, FutureListener<Bitmap> { + private final long mVersion; + private Future<Bitmap> mFuture; + + public ScreenNailListener(long version) { + mVersion = version; + } + + public void onFutureDone(Future<Bitmap> future) { + mFuture = future; + mMainHandler.sendMessage( + mMainHandler.obtainMessage(MSG_RUN_OBJECT, this)); + } + + public void run() { + updateScreenNail(mVersion, mFuture); + } + } + + private static class ImageEntry { + public int requestedBits = 0; + public int rotation; + public BitmapRegionDecoder fullImage; + public Bitmap screenNail; + public Future<Bitmap> screenNailTask; + public Future<BitmapRegionDecoder> fullImageTask; + public boolean failToLoad = false; + } + + private class SourceListener implements ContentListener { + public void onContentDirty() { + if (mReloadTask != null) mReloadTask.notifyDirty(); + } + } + + private <T> T executeAndWait(Callable<T> callable) { + FutureTask<T> task = new FutureTask<T>(callable); + mMainHandler.sendMessage( + mMainHandler.obtainMessage(MSG_RUN_OBJECT, task)); + try { + return task.get(); + } catch (InterruptedException e) { + return null; + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + private static class UpdateInfo { + public long version; + public boolean reloadContent; + public Path target; + public int indexHint; + public int contentStart; + public int contentEnd; + + public int size; + public ArrayList<MediaItem> items; + } + + private class GetUpdateInfo implements Callable<UpdateInfo> { + + private boolean needContentReload() { + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + if (mData[i % DATA_CACHE_SIZE] == null) return true; + } + MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE]; + return current == null || current.getPath() != mItemPath; + } + + @Override + public UpdateInfo call() throws Exception { + UpdateInfo info = new UpdateInfo(); + info.version = mSourceVersion; + info.reloadContent = needContentReload(); + info.target = mItemPath; + info.indexHint = mCurrentIndex; + info.contentStart = mContentStart; + info.contentEnd = mContentEnd; + info.size = mSize; + return info; + } + } + + private class UpdateContent implements Callable<Void> { + UpdateInfo mUpdateInfo; + + public UpdateContent(UpdateInfo updateInfo) { + mUpdateInfo = updateInfo; + } + + @Override + public Void call() throws Exception { + UpdateInfo info = mUpdateInfo; + mSourceVersion = info.version; + + if (info.size != mSize) { + mSize = info.size; + if (mContentEnd > mSize) mContentEnd = mSize; + if (mActiveEnd > mSize) mActiveEnd = mSize; + } + + if (info.indexHint == MediaSet.INDEX_NOT_FOUND) { + // The image has been deleted, clear mItemPath, the + // mCurrentIndex will be updated in the updateCurrentItem(). + mItemPath = null; + updateCurrentItem(); + } else { + mCurrentIndex = info.indexHint; + } + + updateSlidingWindow(); + + if (info.items != null) { + int start = Math.max(info.contentStart, mContentStart); + int end = Math.min(info.contentStart + info.items.size(), mContentEnd); + int dataIndex = start % DATA_CACHE_SIZE; + for (int i = start; i < end; ++i) { + mData[dataIndex] = info.items.get(i - info.contentStart); + if (++dataIndex == DATA_CACHE_SIZE) dataIndex = 0; + } + } + if (mItemPath == null) { + MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE]; + mItemPath = current == null ? null : current.getPath(); + } + updateImageCache(); + updateTileProvider(); + updateImageRequests(); + fireModelInvalidated(); + return null; + } + + private void updateCurrentItem() { + if (mSize == 0) return; + if (mCurrentIndex >= mSize) { + mCurrentIndex = mSize - 1; + mPhotoView.notifyOnNewImage(); + mPhotoView.startSlideInAnimation(PhotoView.TRANS_SLIDE_IN_LEFT); + } else { + mPhotoView.notifyOnNewImage(); + mPhotoView.startSlideInAnimation(PhotoView.TRANS_SLIDE_IN_RIGHT); + } + } + } + + private class ReloadTask extends Thread { + private volatile boolean mActive = true; + private volatile boolean mDirty = true; + + private boolean mIsLoading = false; + + private void updateLoading(boolean loading) { + if (mIsLoading == loading) return; + mIsLoading = loading; + mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH); + } + + @Override + public void run() { + while (mActive) { + synchronized (this) { + if (!mDirty && mActive) { + updateLoading(false); + Utils.waitWithoutInterrupt(this); + continue; + } + } + mDirty = false; + UpdateInfo info = executeAndWait(new GetUpdateInfo()); + synchronized (DataManager.LOCK) { + updateLoading(true); + long version = mSource.reload(); + if (info.version != version) { + info.reloadContent = true; + info.size = mSource.getMediaItemCount(); + } + if (!info.reloadContent) continue; + info.items = mSource.getMediaItem(info.contentStart, info.contentEnd); + MediaItem item = findCurrentMediaItem(info); + if (item == null || item.getPath() != info.target) { + info.indexHint = findIndexOfTarget(info); + } + } + executeAndWait(new UpdateContent(info)); + } + } + + public synchronized void notifyDirty() { + mDirty = true; + notifyAll(); + } + + public synchronized void terminate() { + mActive = false; + notifyAll(); + } + + private MediaItem findCurrentMediaItem(UpdateInfo info) { + ArrayList<MediaItem> items = info.items; + int index = info.indexHint - info.contentStart; + return index < 0 || index >= items.size() ? null : items.get(index); + } + + private int findIndexOfTarget(UpdateInfo info) { + if (info.target == null) return info.indexHint; + ArrayList<MediaItem> items = info.items; + + // First, try to find the item in the data just loaded + if (items != null) { + for (int i = 0, n = items.size(); i < n; ++i) { + if (items.get(i).getPath() == info.target) return i + info.contentStart; + } + } + + // Not found, find it in mSource. + return mSource.getIndexOfItem(info.target, info.indexHint); + } + } +} diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java new file mode 100644 index 000000000..f28eb221d --- /dev/null +++ b/src/com/android/gallery3d/app/PhotoPage.java @@ -0,0 +1,581 @@ +/* + * 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.app; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaDetails; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.MtpDevice; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.picasasource.PicasaSource; +import com.android.gallery3d.ui.DetailsWindow; +import com.android.gallery3d.ui.DetailsWindow.CloseListener; +import com.android.gallery3d.ui.DetailsWindow.DetailsSource; +import com.android.gallery3d.ui.FilmStripView; +import com.android.gallery3d.ui.GLCanvas; +import com.android.gallery3d.ui.GLView; +import com.android.gallery3d.ui.ImportCompleteListener; +import com.android.gallery3d.ui.MenuExecutor; +import com.android.gallery3d.ui.PhotoView; +import com.android.gallery3d.ui.PositionRepository; +import com.android.gallery3d.ui.PositionRepository.Position; +import com.android.gallery3d.ui.SelectionManager; +import com.android.gallery3d.ui.SynchronizedHandler; +import com.android.gallery3d.ui.UserInteractionListener; +import com.android.gallery3d.util.GalleryUtils; + +import android.app.ActionBar; +import android.app.ActionBar.OnMenuVisibilityListener; +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.WindowManager; +import android.widget.ShareActionProvider; +import android.widget.Toast; + +public class PhotoPage extends ActivityState + implements PhotoView.PhotoTapListener, FilmStripView.Listener, + UserInteractionListener { + private static final String TAG = "PhotoPage"; + + private static final int MSG_HIDE_BARS = 1; + private static final int HIDE_BARS_TIMEOUT = 3500; + + private static final int REQUEST_SLIDESHOW = 1; + private static final int REQUEST_CROP = 2; + private static final int REQUEST_CROP_PICASA = 3; + + public static final String KEY_MEDIA_SET_PATH = "media-set-path"; + public static final String KEY_MEDIA_ITEM_PATH = "media-item-path"; + public static final String KEY_INDEX_HINT = "index-hint"; + + private GalleryApp mApplication; + private SelectionManager mSelectionManager; + + private PhotoView mPhotoView; + private PhotoPage.Model mModel; + private FilmStripView mFilmStripView; + private DetailsWindow mDetailsWindow; + private boolean mShowDetails; + + // mMediaSet could be null if there is no KEY_MEDIA_SET_PATH supplied. + // E.g., viewing a photo in gmail attachment + private MediaSet mMediaSet; + private Menu mMenu; + + private Intent mResultIntent = new Intent(); + private int mCurrentIndex = 0; + private Handler mHandler; + private boolean mShowBars; + private ActionBar mActionBar; + private MyMenuVisibilityListener mMenuVisibilityListener; + private boolean mIsMenuVisible; + private boolean mIsInteracting; + private MediaItem mCurrentPhoto = null; + private MenuExecutor mMenuExecutor; + private boolean mIsActive; + private ShareActionProvider mShareActionProvider; + + public static interface Model extends PhotoView.Model { + public void resume(); + public void pause(); + public boolean isEmpty(); + public MediaItem getCurrentMediaItem(); + public int getCurrentIndex(); + public void setCurrentPhoto(Path path, int indexHint); + } + + private class MyMenuVisibilityListener implements OnMenuVisibilityListener { + public void onMenuVisibilityChanged(boolean isVisible) { + mIsMenuVisible = isVisible; + refreshHidingMessage(); + } + } + + private GLView mRootPane = new GLView() { + + @Override + protected void renderBackground(GLCanvas view) { + view.clearBuffer(); + } + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + mPhotoView.layout(0, 0, right - left, bottom - top); + PositionRepository.getInstance(mActivity).setOffset(0, 0); + int filmStripHeight = 0; + if (mFilmStripView != null) { + mFilmStripView.measure( + MeasureSpec.makeMeasureSpec(right - left, MeasureSpec.EXACTLY), + MeasureSpec.UNSPECIFIED); + filmStripHeight = mFilmStripView.getMeasuredHeight(); + mFilmStripView.layout(0, bottom - top - filmStripHeight, + right - left, bottom - top); + } + if (mShowDetails) { + mDetailsWindow.measure( + MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + int width = mDetailsWindow.getMeasuredWidth(); + int viewTop = GalleryActionBar.getHeight((Activity) mActivity); + mDetailsWindow.layout( + 0, viewTop, width, bottom - top - filmStripHeight); + } + } + }; + + @Override + public void onCreate(Bundle data, Bundle restoreState) { + mActionBar = ((Activity) mActivity).getActionBar(); + mSelectionManager = new SelectionManager(mActivity, false); + mMenuExecutor = new MenuExecutor(mActivity, mSelectionManager); + + mPhotoView = new PhotoView(mActivity); + mPhotoView.setPhotoTapListener(this); + mRootPane.addComponent(mPhotoView); + mApplication = (GalleryApp)((Activity) mActivity).getApplication(); + + String setPathString = data.getString(KEY_MEDIA_SET_PATH); + Path itemPath = Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH)); + + if (setPathString != null) { + mMediaSet = mActivity.getDataManager().getMediaSet(setPathString); + mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0); + mMediaSet = (MediaSet) + mActivity.getDataManager().getMediaObject(setPathString); + if (mMediaSet == null) { + Log.w(TAG, "failed to restore " + setPathString); + } + PhotoDataAdapter pda = new PhotoDataAdapter( + mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex); + mModel = pda; + mPhotoView.setModel(mModel); + + Config.PhotoPage config = Config.PhotoPage.get((Context) mActivity); + + mFilmStripView = new FilmStripView(mActivity, mMediaSet, + config.filmstripTopMargin, config.filmstripMidMargin, config.filmstripBottomMargin, + config.filmstripContentSize, config.filmstripThumbSize, config.filmstripBarSize, + config.filmstripGripSize, config.filmstripGripWidth); + mRootPane.addComponent(mFilmStripView); + mFilmStripView.setListener(this); + mFilmStripView.setUserInteractionListener(this); + mFilmStripView.setFocusIndex(mCurrentIndex); + mFilmStripView.setStartIndex(mCurrentIndex); + + mResultIntent.putExtra(KEY_INDEX_HINT, mCurrentIndex); + setStateResult(Activity.RESULT_OK, mResultIntent); + + pda.setDataListener(new PhotoDataAdapter.DataListener() { + + public void onPhotoChanged(int index, Path item) { + mFilmStripView.setFocusIndex(index); + mCurrentIndex = index; + mResultIntent.putExtra(KEY_INDEX_HINT, index); + if (item != null) { + mResultIntent.putExtra(KEY_MEDIA_ITEM_PATH, item.toString()); + MediaItem photo = mModel.getCurrentMediaItem(); + if (photo != null) updateCurrentPhoto(photo); + } else { + mResultIntent.removeExtra(KEY_MEDIA_ITEM_PATH); + } + setStateResult(Activity.RESULT_OK, mResultIntent); + } + + @Override + public void onLoadingFinished() { + GalleryUtils.setSpinnerVisibility((Activity) mActivity, false); + if (!mModel.isEmpty()) { + MediaItem photo = mModel.getCurrentMediaItem(); + if (photo != null) updateCurrentPhoto(photo); + } else if (mIsActive) { + mActivity.getStateManager().finishState(PhotoPage.this); + } + } + + + @Override + public void onLoadingStarted() { + GalleryUtils.setSpinnerVisibility((Activity) mActivity, true); + } + }); + } else { + // Get default media set by the URI + MediaItem mediaItem = (MediaItem) + mActivity.getDataManager().getMediaObject(itemPath); + mModel = new SinglePhotoDataAdapter(mActivity, mPhotoView, mediaItem); + mPhotoView.setModel(mModel); + updateCurrentPhoto(mediaItem); + } + mHandler = new SynchronizedHandler(mActivity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_HIDE_BARS: { + hideBars(); + break; + } + default: throw new AssertionError(message.what); + } + } + }; + + // start the opening animation + mPhotoView.setOpenedItem(itemPath); + } + + private void updateCurrentPhoto(MediaItem photo) { + if (mCurrentPhoto == photo) return; + mCurrentPhoto = photo; + if (mCurrentPhoto == null) return; + updateMenuOperations(); + if (mShowDetails) { + mDetailsWindow.reloadDetails(mModel.getCurrentIndex()); + } + String title = photo.getName(); + if (title != null) mActionBar.setTitle(title); + mPhotoView.showVideoPlayIcon(photo.getMediaType() + == MediaObject.MEDIA_TYPE_VIDEO); + + // If we have an ActionBar then we update the share intent + if (mShareActionProvider != null) { + Path path = photo.getPath(); + DataManager manager = mActivity.getDataManager(); + int type = manager.getMediaType(path); + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType(MenuExecutor.getMimeType(type)); + intent.putExtra(Intent.EXTRA_STREAM, manager.getContentUri(path)); + mShareActionProvider.setShareIntent(intent); + } + } + + private void updateMenuOperations() { + if (mCurrentPhoto == null || mMenu == null) return; + int supportedOperations = mCurrentPhoto.getSupportedOperations(); + if (!GalleryUtils.isEditorAvailable((Context) mActivity, "image/*")) { + supportedOperations &= ~MediaObject.SUPPORT_EDIT; + } + MenuExecutor.updateMenuOperation(mMenu, supportedOperations); + } + + private void showBars() { + if (mShowBars) return; + mShowBars = true; + mActionBar.show(); + WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes(); + params.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE; + ((Activity) mActivity).getWindow().setAttributes(params); + if (mFilmStripView != null) { + mFilmStripView.show(); + } + } + + private void hideBars() { + if (!mShowBars) return; + mShowBars = false; + mActionBar.hide(); + WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes(); + params.systemUiVisibility = View. SYSTEM_UI_FLAG_LOW_PROFILE; + ((Activity) mActivity).getWindow().setAttributes(params); + if (mFilmStripView != null) { + mFilmStripView.hide(); + } + } + + private void refreshHidingMessage() { + mHandler.removeMessages(MSG_HIDE_BARS); + if (!mIsMenuVisible && !mIsInteracting) { + mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT); + } + } + + public void onUserInteraction() { + showBars(); + refreshHidingMessage(); + } + + public void onUserInteractionTap() { + if (mShowBars) { + hideBars(); + mHandler.removeMessages(MSG_HIDE_BARS); + } else { + showBars(); + refreshHidingMessage(); + } + } + + public void onUserInteractionBegin() { + showBars(); + mIsInteracting = true; + refreshHidingMessage(); + } + + public void onUserInteractionEnd() { + mIsInteracting = false; + refreshHidingMessage(); + } + + @Override + protected void onBackPressed() { + if (mShowDetails) { + hideDetails(); + } else { + PositionRepository repository = PositionRepository.getInstance(mActivity); + repository.clear(); + if (mCurrentPhoto != null) { + Position position = new Position(); + position.x = mRootPane.getWidth() / 2; + position.y = mRootPane.getHeight() / 2; + position.z = -1000; + repository.putPosition( + Long.valueOf(System.identityHashCode(mCurrentPhoto.getPath())), + position); + } + super.onBackPressed(); + } + } + + @Override + protected boolean onCreateActionBar(Menu menu) { + MenuInflater inflater = ((Activity) mActivity).getMenuInflater(); + inflater.inflate(R.menu.photo, menu); + menu.findItem(R.id.action_slideshow).setVisible( + mMediaSet != null && !(mMediaSet instanceof MtpDevice)); + mShareActionProvider = GalleryActionBar.initializeShareActionProvider(menu); + mMenu = menu; + mShowBars = true; + updateMenuOperations(); + return true; + } + + @Override + protected boolean onItemSelected(MenuItem item) { + MediaItem current = mModel.getCurrentMediaItem(); + + if (current == null) { + // item is not ready, ignore + return true; + } + + int currentIndex = mModel.getCurrentIndex(); + Path path = current.getPath(); + + DataManager manager = mActivity.getDataManager(); + int action = item.getItemId(); + switch (action) { + case R.id.action_slideshow: { + Bundle data = new Bundle(); + data.putString(SlideshowPage.KEY_SET_PATH, + mMediaSet.getPath().toString()); + data.putInt(SlideshowPage.KEY_PHOTO_INDEX, currentIndex); + data.putBoolean(SlideshowPage.KEY_REPEAT, true); + mActivity.getStateManager().startStateForResult( + SlideshowPage.class, REQUEST_SLIDESHOW, data); + return true; + } + case R.id.action_crop: { + Activity activity = (Activity) mActivity; + Intent intent = new Intent(CropImage.CROP_ACTION); + intent.setClass(activity, CropImage.class); + intent.setData(manager.getContentUri(path)); + activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current) + ? REQUEST_CROP_PICASA + : REQUEST_CROP); + return true; + } + case R.id.action_details: { + if (mShowDetails) { + hideDetails(); + } else { + showDetails(currentIndex); + } + return true; + } + case R.id.action_setas: + case R.id.action_confirm_delete: + case R.id.action_rotate_ccw: + case R.id.action_rotate_cw: + case R.id.action_show_on_map: + case R.id.action_edit: + mSelectionManager.deSelectAll(); + mSelectionManager.toggle(path); + mMenuExecutor.onMenuClicked(item, null); + return true; + case R.id.action_import: + mSelectionManager.deSelectAll(); + mSelectionManager.toggle(path); + mMenuExecutor.onMenuClicked(item, + new ImportCompleteListener(mActivity)); + return true; + default : + return false; + } + } + + private void hideDetails() { + mShowDetails = false; + mDetailsWindow.hide(); + } + + private void showDetails(int index) { + mShowDetails = true; + if (mDetailsWindow == null) { + mDetailsWindow = new DetailsWindow(mActivity, new MyDetailsSource()); + mDetailsWindow.setCloseListener(new CloseListener() { + public void onClose() { + hideDetails(); + } + }); + mRootPane.addComponent(mDetailsWindow); + } + mDetailsWindow.reloadDetails(index); + mDetailsWindow.show(); + } + + public void onSingleTapUp(int x, int y) { + MediaItem item = mModel.getCurrentMediaItem(); + if (item == null) { + // item is not ready, ignore + return; + } + + boolean playVideo = + (item.getSupportedOperations() & MediaItem.SUPPORT_PLAY) != 0; + + if (playVideo) { + // determine if the point is at center (1/6) of the photo view. + // (The position of the "play" icon is at center (1/6) of the photo) + int w = mPhotoView.getWidth(); + int h = mPhotoView.getHeight(); + playVideo = (Math.abs(x - w / 2) * 12 <= w) + && (Math.abs(y - h / 2) * 12 <= h); + } + + if (playVideo) { + playVideo((Activity) mActivity, item.getPlayUri(), item.getName()); + } else { + onUserInteractionTap(); + } + } + + public static void playVideo(Activity activity, Uri uri, String title) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW) + .setDataAndType(uri, "video/*"); + intent.putExtra(Intent.EXTRA_TITLE, title); + activity.startActivity(intent); + } catch (ActivityNotFoundException e) { + Toast.makeText(activity, activity.getString(R.string.video_err), + Toast.LENGTH_SHORT).show(); + } + } + + // Called by FileStripView + public void onSlotSelected(int slotIndex) { + ((PhotoDataAdapter) mModel).jumpTo(slotIndex); + } + + @Override + protected void onStateResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_CROP: + if (resultCode == Activity.RESULT_OK) { + if (data == null) break; + Path path = mApplication + .getDataManager().findPathByUri(data.getData()); + if (path != null) { + mModel.setCurrentPhoto(path, mCurrentIndex); + } + } + break; + case REQUEST_CROP_PICASA: { + int message = resultCode == Activity.RESULT_OK + ? R.string.crop_saved + : R.string.crop_not_saved; + Toast.makeText(mActivity.getAndroidContext(), + message, Toast.LENGTH_SHORT).show(); + break; + } + case REQUEST_SLIDESHOW: { + if (data == null) break; + String path = data.getStringExtra(SlideshowPage.KEY_ITEM_PATH); + int index = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0); + if (path != null) { + mModel.setCurrentPhoto(Path.fromString(path), index); + } + } + } + } + + @Override + public void onPause() { + super.onPause(); + mIsActive = false; + if (mFilmStripView != null) { + mFilmStripView.pause(); + } + if (mDetailsWindow != null) { + mDetailsWindow.pause(); + } + mPhotoView.pause(); + mModel.pause(); + mHandler.removeMessages(MSG_HIDE_BARS); + mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener); + } + + @Override + protected void onResume() { + super.onResume(); + mIsActive = true; + setContentPane(mRootPane); + mModel.resume(); + mPhotoView.resume(); + if (mFilmStripView != null) { + mFilmStripView.resume(); + } + if (mMenuVisibilityListener == null) { + mMenuVisibilityListener = new MyMenuVisibilityListener(); + } + mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener); + onUserInteraction(); + } + + private class MyDetailsSource implements DetailsSource { + public MediaDetails getDetails() { + return mModel.getCurrentMediaItem().getDetails(); + } + public int size() { + return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1; + } + public int findIndex(int indexHint) { + return indexHint; + } + } +} diff --git a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java new file mode 100644 index 000000000..11e0013cc --- /dev/null +++ b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java @@ -0,0 +1,181 @@ +/* + * 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.app; + +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.PhotoView; +import com.android.gallery3d.ui.PhotoView.ImageData; +import com.android.gallery3d.ui.SynchronizedHandler; +import com.android.gallery3d.ui.TileImageViewAdapter; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.ThreadPool; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Message; + +public class SinglePhotoDataAdapter extends TileImageViewAdapter + implements PhotoPage.Model { + + private static final String TAG = "SinglePhotoDataAdapter"; + private static final int SIZE_BACKUP = 640; + private static final int MSG_UPDATE_IMAGE = 1; + + private MediaItem mItem; + private boolean mHasFullImage; + private Future<?> mTask; + private BitmapRegionDecoder mDecoder; + private Bitmap mBackup; + private Handler mHandler; + + private PhotoView mPhotoView; + private ThreadPool mThreadPool; + + public SinglePhotoDataAdapter( + GalleryActivity activity, PhotoView view, MediaItem item) { + mItem = Utils.checkNotNull(item); + mHasFullImage = (item.getSupportedOperations() & + MediaItem.SUPPORT_FULL_IMAGE) != 0; + mPhotoView = Utils.checkNotNull(view); + mHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + @SuppressWarnings("unchecked") + public void handleMessage(Message message) { + Utils.assertTrue(message.what == MSG_UPDATE_IMAGE); + if (mHasFullImage) { + onDecodeLargeComplete((Future<BitmapRegionDecoder>) + message.obj); + } else { + onDecodeThumbComplete((Future<Bitmap>) message.obj); + } + } + }; + mThreadPool = activity.getThreadPool(); + } + + private FutureListener<BitmapRegionDecoder> mLargeListener = + new FutureListener<BitmapRegionDecoder>() { + public void onFutureDone(Future<BitmapRegionDecoder> future) { + mHandler.sendMessage( + mHandler.obtainMessage(MSG_UPDATE_IMAGE, future)); + } + }; + + private FutureListener<Bitmap> mThumbListener = + new FutureListener<Bitmap>() { + public void onFutureDone(Future<Bitmap> future) { + mHandler.sendMessage( + mHandler.obtainMessage(MSG_UPDATE_IMAGE, future)); + } + }; + + public boolean isEmpty() { + return false; + } + + public int getImageRotation() { + return mItem.getRotation(); + } + + private void onDecodeLargeComplete(Future<BitmapRegionDecoder> future) { + try { + mDecoder = future.get(); + if (mDecoder == null) return; + int width = mDecoder.getWidth(); + int height = mDecoder.getHeight(); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = BitmapUtils.computeSampleSizeLarger( + width, height, SIZE_BACKUP); + mBackup = mDecoder.decodeRegion( + new Rect(0, 0, width, height), options); + setBackupImage(mBackup, width, height); + setRegionDecoder(mDecoder); + mPhotoView.notifyImageInvalidated(0); + } catch (Throwable t) { + Log.w(TAG, "fail to decode large", t); + } + } + + private void onDecodeThumbComplete(Future<Bitmap> future) { + try { + mBackup = future.get(); + if (mBackup == null) return; + setBackupImage(mBackup, mBackup.getWidth(), mBackup.getHeight()); + mPhotoView.notifyOnNewImage(); + mPhotoView.notifyImageInvalidated(0); // the current image + } catch (Throwable t) { + Log.w(TAG, "fail to decode thumb", t); + } + } + + public void resume() { + if (mTask == null) { + if (mHasFullImage) { + mTask = mThreadPool.submit( + mItem.requestLargeImage(), mLargeListener); + } else { + mTask = mThreadPool.submit( + mItem.requestImage(MediaItem.TYPE_THUMBNAIL), + mThumbListener); + } + } + } + + public void pause() { + Future<?> task = mTask; + task.cancel(); + task.waitDone(); + if (task.get() == null) { + mTask = null; + } + } + + public ImageData getNextImage() { + return null; + } + + public ImageData getPreviousImage() { + return null; + } + + public void next() { + throw new UnsupportedOperationException(); + } + + public void previous() { + throw new UnsupportedOperationException(); + } + + public MediaItem getCurrentMediaItem() { + return mItem; + } + + public int getCurrentIndex() { + return 0; + } + + public void setCurrentPhoto(Path path, int indexHint) { + // ignore + } +} diff --git a/src/com/android/gallery3d/app/SlideshowDataAdapter.java b/src/com/android/gallery3d/app/SlideshowDataAdapter.java new file mode 100644 index 000000000..6f9b98e8e --- /dev/null +++ b/src/com/android/gallery3d/app/SlideshowDataAdapter.java @@ -0,0 +1,187 @@ +/* + * 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.app; + +import com.android.gallery3d.app.SlideshowPage.Slide; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.ThreadPool; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.graphics.Bitmap; + +import java.util.LinkedList; +import java.util.concurrent.atomic.AtomicBoolean; + +public class SlideshowDataAdapter implements SlideshowPage.Model { + @SuppressWarnings("unused") + private static final String TAG = "SlideshowDataAdapter"; + + private static final int IMAGE_QUEUE_CAPACITY = 3; + + public interface SlideshowSource { + public void addContentListener(ContentListener listener); + public void removeContentListener(ContentListener listener); + public long reload(); + public MediaItem getMediaItem(int index); + } + + private final SlideshowSource mSource; + + private int mLoadIndex = 0; + private int mNextOutput = 0; + private boolean mIsActive = false; + private boolean mNeedReset; + private boolean mDataReady; + + private final LinkedList<Slide> mImageQueue = new LinkedList<Slide>(); + + private Future<Void> mReloadTask; + private final ThreadPool mThreadPool; + + private long mDataVersion = MediaObject.INVALID_DATA_VERSION; + private final AtomicBoolean mNeedReload = new AtomicBoolean(false); + private final SourceListener mSourceListener = new SourceListener(); + + public SlideshowDataAdapter(GalleryContext context, SlideshowSource source, int index) { + mSource = source; + mLoadIndex = index; + mNextOutput = index; + mThreadPool = context.getThreadPool(); + } + + public MediaItem loadItem() { + if (mNeedReload.compareAndSet(true, false)) { + long v = mSource.reload(); + if (v != mDataVersion) { + mDataVersion = v; + mNeedReset = true; + return null; + } + } + return mSource.getMediaItem(mLoadIndex); + } + + private class ReloadTask implements Job<Void> { + public Void run(JobContext jc) { + while (true) { + synchronized (SlideshowDataAdapter.this) { + while (mIsActive && (!mDataReady + || mImageQueue.size() >= IMAGE_QUEUE_CAPACITY)) { + try { + SlideshowDataAdapter.this.wait(); + } catch (InterruptedException ex) { + // ignored. + } + continue; + } + } + if (!mIsActive) return null; + mNeedReset = false; + + MediaItem item = loadItem(); + + if (mNeedReset) { + synchronized (SlideshowDataAdapter.this) { + mImageQueue.clear(); + mLoadIndex = mNextOutput; + } + continue; + } + + if (item == null) { + synchronized (SlideshowDataAdapter.this) { + if (!mNeedReload.get()) mDataReady = false; + SlideshowDataAdapter.this.notifyAll(); + } + continue; + } + + Bitmap bitmap = item + .requestImage(MediaItem.TYPE_THUMBNAIL) + .run(jc); + + if (bitmap != null) { + synchronized (SlideshowDataAdapter.this) { + mImageQueue.addLast( + new Slide(item, mLoadIndex, bitmap)); + if (mImageQueue.size() == 1) { + SlideshowDataAdapter.this.notifyAll(); + } + } + } + ++mLoadIndex; + } + } + } + + private class SourceListener implements ContentListener { + public void onContentDirty() { + synchronized (SlideshowDataAdapter.this) { + mNeedReload.set(true); + mDataReady = true; + SlideshowDataAdapter.this.notifyAll(); + } + } + } + + private synchronized Slide innerNextBitmap() { + while (mIsActive && mDataReady && mImageQueue.isEmpty()) { + try { + wait(); + } catch (InterruptedException t) { + throw new AssertionError(); + } + } + if (mImageQueue.isEmpty()) return null; + mNextOutput++; + this.notifyAll(); + return mImageQueue.removeFirst(); + } + + public Future<Slide> nextSlide(FutureListener<Slide> listener) { + return mThreadPool.submit(new Job<Slide>() { + public Slide run(JobContext jc) { + jc.setMode(ThreadPool.MODE_NONE); + return innerNextBitmap(); + } + }, listener); + } + + public void pause() { + synchronized (this) { + mIsActive = false; + notifyAll(); + } + mSource.removeContentListener(mSourceListener); + mReloadTask.cancel(); + mReloadTask.waitDone(); + mReloadTask = null; + } + + public synchronized void resume() { + mIsActive = true; + mSource.addContentListener(mSourceListener); + mNeedReload.set(true); + mDataReady = true; + mReloadTask = mThreadPool.submit(new ReloadTask()); + } +} diff --git a/src/com/android/gallery3d/app/SlideshowDream.java b/src/com/android/gallery3d/app/SlideshowDream.java new file mode 100644 index 000000000..f4abe86ab --- /dev/null +++ b/src/com/android/gallery3d/app/SlideshowDream.java @@ -0,0 +1,28 @@ +package com.android.gallery3d.app; + +import android.app.Activity; +import android.content.Intent; +import android.support.v13.dreams.BasicDream; +import android.graphics.Canvas; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.view.View; +import android.widget.ImageView; +import android.widget.ViewFlipper; + +public class SlideshowDream extends BasicDream { + @Override + public void onCreate(Bundle bndl) { + super.onCreate(bndl); + Intent i = new Intent( + Intent.ACTION_VIEW, + android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI) +// Uri.fromFile(Environment.getExternalStoragePublicDirectory( +// Environment.DIRECTORY_PICTURES))) + .putExtra(Gallery.EXTRA_SLIDESHOW, true) + .setFlags(getIntent().getFlags()); + startActivity(i); + finish(); + } +} diff --git a/src/com/android/gallery3d/app/SlideshowPage.java b/src/com/android/gallery3d/app/SlideshowPage.java new file mode 100644 index 000000000..cdf9308ec --- /dev/null +++ b/src/com/android/gallery3d/app/SlideshowPage.java @@ -0,0 +1,338 @@ +/* + * 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.app; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.ui.GLCanvas; +import com.android.gallery3d.ui.GLView; +import com.android.gallery3d.ui.SlideshowView; +import com.android.gallery3d.ui.SynchronizedHandler; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.view.MotionEvent; + +import java.util.ArrayList; +import java.util.Random; + +public class SlideshowPage extends ActivityState { + private static final String TAG = "SlideshowPage"; + + public static final String KEY_SET_PATH = "media-set-path"; + public static final String KEY_ITEM_PATH = "media-item-path"; + public static final String KEY_PHOTO_INDEX = "photo-index"; + public static final String KEY_RANDOM_ORDER = "random-order"; + public static final String KEY_REPEAT = "repeat"; + + private static final long SLIDESHOW_DELAY = 3000; // 3 seconds + + private static final int MSG_LOAD_NEXT_BITMAP = 1; + private static final int MSG_SHOW_PENDING_BITMAP = 2; + + public static interface Model { + public void pause(); + public void resume(); + public Future<Slide> nextSlide(FutureListener<Slide> listener); + } + + public static class Slide { + public Bitmap bitmap; + public MediaItem item; + public int index; + + public Slide(MediaItem item, int index, Bitmap bitmap) { + this.bitmap = bitmap; + this.item = item; + this.index = index; + } + } + + private Handler mHandler; + private Model mModel; + private SlideshowView mSlideshowView; + + private Slide mPendingSlide = null; + private boolean mIsActive = false; + private WakeLock mWakeLock; + private Intent mResultIntent = new Intent(); + + private GLView mRootPane = new GLView() { + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + mSlideshowView.layout(0, 0, right - left, bottom - top); + } + + @Override + protected boolean onTouch(MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_UP) { + onBackPressed(); + } + return true; + } + + @Override + protected void renderBackground(GLCanvas canvas) { + canvas.clearBuffer(); + } + }; + + @Override + public void onCreate(Bundle data, Bundle restoreState) { + mFlags |= (FLAG_HIDE_ACTION_BAR | FLAG_HIDE_STATUS_BAR); + + PowerManager pm = (PowerManager) mActivity.getAndroidContext() + .getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK + | PowerManager.ON_AFTER_RELEASE, TAG); + + mHandler = new SynchronizedHandler(mActivity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_SHOW_PENDING_BITMAP: + showPendingBitmap(); + break; + case MSG_LOAD_NEXT_BITMAP: + loadNextBitmap(); + break; + default: throw new AssertionError(); + } + } + }; + initializeViews(); + initializeData(data); + } + + private void loadNextBitmap() { + mModel.nextSlide(new FutureListener<Slide>() { + public void onFutureDone(Future<Slide> future) { + mPendingSlide = future.get(); + mHandler.sendEmptyMessage(MSG_SHOW_PENDING_BITMAP); + } + }); + } + + private void showPendingBitmap() { + // mPendingBitmap could be null, if + // 1.) there is no more items + // 2.) mModel is paused + Slide slide = mPendingSlide; + if (slide == null) { + if (mIsActive) { + mActivity.getStateManager().finishState(SlideshowPage.this); + } + return; + } + + mSlideshowView.next(slide.bitmap, slide.item.getRotation()); + + setStateResult(Activity.RESULT_OK, mResultIntent + .putExtra(KEY_ITEM_PATH, slide.item.getPath().toString()) + .putExtra(KEY_PHOTO_INDEX, slide.index)); + mHandler.sendEmptyMessageDelayed(MSG_LOAD_NEXT_BITMAP, + SLIDESHOW_DELAY); + } + + @Override + public void onPause() { + super.onPause(); + mWakeLock.release(); + mIsActive = false; + mModel.pause(); + mSlideshowView.release(); + + mHandler.removeMessages(MSG_LOAD_NEXT_BITMAP); + mHandler.removeMessages(MSG_SHOW_PENDING_BITMAP); + } + + @Override + public void onResume() { + super.onResume(); + mWakeLock.acquire(); + mIsActive = true; + mModel.resume(); + + if (mPendingSlide != null) { + showPendingBitmap(); + } else { + loadNextBitmap(); + } + } + + private void initializeData(Bundle data) { + String mediaPath = data.getString(KEY_SET_PATH); + boolean random = data.getBoolean(KEY_RANDOM_ORDER, false); + MediaSet mediaSet = mActivity.getDataManager().getMediaSet(mediaPath); + + if (random) { + boolean repeat = data.getBoolean(KEY_REPEAT); + mModel = new SlideshowDataAdapter( + mActivity, new ShuffleSource(mediaSet, repeat), 0); + setStateResult(Activity.RESULT_OK, + mResultIntent.putExtra(KEY_PHOTO_INDEX, 0)); + } else { + int index = data.getInt(KEY_PHOTO_INDEX); + boolean repeat = data.getBoolean(KEY_REPEAT); + mModel = new SlideshowDataAdapter(mActivity, + new SequentialSource(mediaSet, repeat), index); + setStateResult(Activity.RESULT_OK, + mResultIntent.putExtra(KEY_PHOTO_INDEX, index)); + } + } + + private void initializeViews() { + mSlideshowView = new SlideshowView(); + mRootPane.addComponent(mSlideshowView); + setContentPane(mRootPane); + } + + private static MediaItem findMediaItem(MediaSet mediaSet, int index) { + for (int i = 0, n = mediaSet.getSubMediaSetCount(); i < n; ++i) { + MediaSet subset = mediaSet.getSubMediaSet(i); + int count = subset.getTotalMediaItemCount(); + if (index < count) { + return findMediaItem(subset, index); + } + index -= count; + } + ArrayList<MediaItem> list = mediaSet.getMediaItem(index, 1); + return list.isEmpty() ? null : list.get(0); + } + + private static class ShuffleSource implements SlideshowDataAdapter.SlideshowSource { + private static final int RETRY_COUNT = 5; + private final MediaSet mMediaSet; + private final Random mRandom = new Random(); + private int mOrder[] = new int[0]; + private boolean mRepeat; + private long mSourceVersion = MediaSet.INVALID_DATA_VERSION; + private int mLastIndex = -1; + + public ShuffleSource(MediaSet mediaSet, boolean repeat) { + mMediaSet = Utils.checkNotNull(mediaSet); + mRepeat = repeat; + } + + public MediaItem getMediaItem(int index) { + if (!mRepeat && index >= mOrder.length) return null; + mLastIndex = mOrder[index % mOrder.length]; + MediaItem item = findMediaItem(mMediaSet, mLastIndex); + for (int i = 0; i < RETRY_COUNT && item == null; ++i) { + Log.w(TAG, "fail to find image: " + mLastIndex); + mLastIndex = mRandom.nextInt(mOrder.length); + item = findMediaItem(mMediaSet, mLastIndex); + } + return item; + } + + public long reload() { + long version = mMediaSet.reload(); + if (version != mSourceVersion) { + mSourceVersion = version; + int count = mMediaSet.getTotalMediaItemCount(); + if (count != mOrder.length) generateOrderArray(count); + } + return version; + } + + private void generateOrderArray(int totalCount) { + if (mOrder.length != totalCount) { + mOrder = new int[totalCount]; + for (int i = 0; i < totalCount; ++i) { + mOrder[i] = i; + } + } + for (int i = totalCount - 1; i > 0; --i) { + Utils.swap(mOrder, i, mRandom.nextInt(i + 1)); + } + if (mOrder[0] == mLastIndex && totalCount > 1) { + Utils.swap(mOrder, 0, mRandom.nextInt(totalCount - 1) + 1); + } + } + + public void addContentListener(ContentListener listener) { + mMediaSet.addContentListener(listener); + } + + public void removeContentListener(ContentListener listener) { + mMediaSet.removeContentListener(listener); + } + } + + private static class SequentialSource implements SlideshowDataAdapter.SlideshowSource { + private static final int DATA_SIZE = 32; + + private ArrayList<MediaItem> mData = new ArrayList<MediaItem>(); + private int mDataStart = 0; + private long mDataVersion = MediaObject.INVALID_DATA_VERSION; + private final MediaSet mMediaSet; + private final boolean mRepeat; + + public SequentialSource(MediaSet mediaSet, boolean repeat) { + mMediaSet = mediaSet; + mRepeat = repeat; + } + + public MediaItem getMediaItem(int index) { + int dataEnd = mDataStart + mData.size(); + + if (mRepeat) { + index = index % mMediaSet.getMediaItemCount(); + } + if (index < mDataStart || index >= dataEnd) { + mData = mMediaSet.getMediaItem(index, DATA_SIZE); + mDataStart = index; + dataEnd = index + mData.size(); + } + + return (index < mDataStart || index >= dataEnd) + ? null + : mData.get(index - mDataStart); + } + + public long reload() { + long version = mMediaSet.reload(); + if (version != mDataVersion) { + mDataVersion = version; + mData.clear(); + } + return mDataVersion; + } + + public void addContentListener(ContentListener listener) { + mMediaSet.addContentListener(listener); + } + + public void removeContentListener(ContentListener listener) { + mMediaSet.removeContentListener(listener); + } + } +} diff --git a/src/com/android/gallery3d/app/StateManager.java b/src/com/android/gallery3d/app/StateManager.java new file mode 100644 index 000000000..b551f693a --- /dev/null +++ b/src/com/android/gallery3d/app/StateManager.java @@ -0,0 +1,277 @@ +/* + * 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.app; + +import com.android.gallery3d.common.Utils; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Parcelable; +import android.view.Menu; +import android.view.MenuItem; + +import java.util.Stack; + +public class StateManager { + @SuppressWarnings("unused") + private static final String TAG = "StateManager"; + private boolean mIsResumed = false; + + private static final String KEY_MAIN = "activity-state"; + private static final String KEY_DATA = "data"; + private static final String KEY_STATE = "bundle"; + private static final String KEY_CLASS = "class"; + + private GalleryActivity mContext; + private Stack<StateEntry> mStack = new Stack<StateEntry>(); + private ActivityState.ResultEntry mResult; + + public StateManager(GalleryActivity context) { + mContext = context; + } + + public void startState(Class<? extends ActivityState> klass, + Bundle data) { + Log.v(TAG, "startState " + klass); + ActivityState state = null; + try { + state = klass.newInstance(); + } catch (Exception e) { + throw new AssertionError(e); + } + if (!mStack.isEmpty()) { + ActivityState top = getTopState(); + if (mIsResumed) top.onPause(); + } + state.initialize(mContext, data); + + mStack.push(new StateEntry(data, state)); + state.onCreate(data, null); + if (mIsResumed) state.resume(); + } + + public void startStateForResult(Class<? extends ActivityState> klass, + int requestCode, Bundle data) { + Log.v(TAG, "startStateForResult " + klass + ", " + requestCode); + ActivityState state = null; + try { + state = klass.newInstance(); + } catch (Exception e) { + throw new AssertionError(e); + } + state.initialize(mContext, data); + state.mResult = new ActivityState.ResultEntry(); + state.mResult.requestCode = requestCode; + + if (!mStack.isEmpty()) { + ActivityState as = getTopState(); + as.mReceivedResults = state.mResult; + if (mIsResumed) as.onPause(); + } else { + mResult = state.mResult; + } + + mStack.push(new StateEntry(data, state)); + state.onCreate(data, null); + if (mIsResumed) state.resume(); + } + + public boolean createOptionsMenu(Menu menu) { + if (!mStack.isEmpty()) { + ((Activity) mContext).setProgressBarIndeterminateVisibility(false); + return getTopState().onCreateActionBar(menu); + } else { + return false; + } + } + + public void resume() { + if (mIsResumed) return; + mIsResumed = true; + if (!mStack.isEmpty()) getTopState().resume(); + } + + public void pause() { + if (!mIsResumed) return; + mIsResumed = false; + if (!mStack.isEmpty()) getTopState().onPause(); + } + + public void notifyActivityResult(int requestCode, int resultCode, Intent data) { + getTopState().onStateResult(requestCode, resultCode, data); + } + + public int getStateCount() { + return mStack.size(); + } + + public boolean itemSelected(MenuItem item) { + if (!mStack.isEmpty()) { + if (mStack.size() > 1 && item.getItemId() == android.R.id.home) { + getTopState().onBackPressed(); + return true; + } else { + return getTopState().onItemSelected(item); + } + } + return false; + } + + public void onBackPressed() { + if (!mStack.isEmpty()) { + getTopState().onBackPressed(); + } + } + + void finishState(ActivityState state) { + Log.v(TAG, "finishState " + state.getClass()); + if (state != mStack.peek().activityState) { + throw new IllegalArgumentException("The stateview to be finished" + + " is not at the top of the stack: " + state + ", " + + mStack.peek().activityState); + } + + // Remove the top state. + mStack.pop(); + if (mIsResumed) state.onPause(); + mContext.getGLRoot().setContentPane(null); + state.onDestroy(); + + if (mStack.isEmpty()) { + Log.v(TAG, "no more state, finish activity"); + Activity activity = (Activity) mContext.getAndroidContext(); + if (mResult != null) { + activity.setResult(mResult.resultCode, mResult.resultData); + } + activity.finish(); + + // The finish() request is rejected (only happens under Monkey), + // so we start the default page instead. + if (!activity.isFinishing()) { + Log.v(TAG, "finish() failed, start default page"); + ((Gallery) mContext).startDefaultPage(); + } + } else { + // Restore the immediately previous state + ActivityState top = mStack.peek().activityState; + if (mIsResumed) top.resume(); + } + } + + void switchState(ActivityState oldState, + Class<? extends ActivityState> klass, Bundle data) { + Log.v(TAG, "switchState " + oldState + ", " + klass); + if (oldState != mStack.peek().activityState) { + throw new IllegalArgumentException("The stateview to be finished" + + " is not at the top of the stack: " + oldState + ", " + + mStack.peek().activityState); + } + // Remove the top state. + mStack.pop(); + if (mIsResumed) oldState.onPause(); + oldState.onDestroy(); + + // Create new state. + ActivityState state = null; + try { + state = klass.newInstance(); + } catch (Exception e) { + throw new AssertionError(e); + } + state.initialize(mContext, data); + mStack.push(new StateEntry(data, state)); + state.onCreate(data, null); + if (mIsResumed) state.resume(); + } + + public void destroy() { + Log.v(TAG, "destroy"); + while (!mStack.isEmpty()) { + mStack.pop().activityState.onDestroy(); + } + mStack.clear(); + } + + @SuppressWarnings("unchecked") + public void restoreFromState(Bundle inState) { + Log.v(TAG, "restoreFromState"); + Parcelable list[] = inState.getParcelableArray(KEY_MAIN); + + for (Parcelable parcelable : list) { + Bundle bundle = (Bundle) parcelable; + Class<? extends ActivityState> klass = + (Class<? extends ActivityState>) bundle.getSerializable(KEY_CLASS); + + Bundle data = bundle.getBundle(KEY_DATA); + Bundle state = bundle.getBundle(KEY_STATE); + + ActivityState activityState; + try { + Log.v(TAG, "restoreFromState " + klass); + activityState = klass.newInstance(); + } catch (Exception e) { + throw new AssertionError(e); + } + activityState.initialize(mContext, data); + activityState.onCreate(data, state); + mStack.push(new StateEntry(data, activityState)); + } + } + + public void saveState(Bundle outState) { + Log.v(TAG, "saveState"); + Parcelable list[] = new Parcelable[mStack.size()]; + + int i = 0; + for (StateEntry entry : mStack) { + Bundle bundle = new Bundle(); + bundle.putSerializable(KEY_CLASS, entry.activityState.getClass()); + bundle.putBundle(KEY_DATA, entry.data); + Bundle state = new Bundle(); + entry.activityState.onSaveState(state); + bundle.putBundle(KEY_STATE, state); + Log.v(TAG, "saveState " + entry.activityState.getClass()); + list[i++] = bundle; + } + outState.putParcelableArray(KEY_MAIN, list); + } + + public boolean hasStateClass(Class<? extends ActivityState> klass) { + for (StateEntry entry : mStack) { + if (klass.isInstance(entry.activityState)) { + return true; + } + } + return false; + } + + public ActivityState getTopState() { + Utils.assertTrue(!mStack.isEmpty()); + return mStack.peek().activityState; + } + + private static class StateEntry { + public Bundle data; + public ActivityState activityState; + + public StateEntry(Bundle data, ActivityState state) { + this.data = data; + this.activityState = state; + } + } +} diff --git a/src/com/android/gallery3d/app/UsbDeviceActivity.java b/src/com/android/gallery3d/app/UsbDeviceActivity.java new file mode 100644 index 000000000..28bd667e3 --- /dev/null +++ b/src/com/android/gallery3d/app/UsbDeviceActivity.java @@ -0,0 +1,47 @@ +/* + * 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.app; + + +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +/* This Activity does nothing but receive USB_DEVICE_ATTACHED events from the + * USB service and springboards to the main Gallery activity + */ +public final class UsbDeviceActivity extends Activity { + + static final String TAG = "UsbDeviceActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // + Intent intent = new Intent(this, Gallery.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + try { + startActivity(intent); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "unable to start Gallery activity", e); + } + finish(); + } +} diff --git a/src/com/android/gallery3d/app/Wallpaper.java b/src/com/android/gallery3d/app/Wallpaper.java new file mode 100644 index 000000000..07a3d5313 --- /dev/null +++ b/src/com/android/gallery3d/app/Wallpaper.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2007 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.app; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.view.Display; + +/** + * Wallpaper picker for the gallery application. This just redirects to the + * standard pick action. + */ +public class Wallpaper extends Activity { + @SuppressWarnings("unused") + private static final String TAG = "Wallpaper"; + + private static final String IMAGE_TYPE = "image/*"; + private static final String KEY_STATE = "activity-state"; + private static final String KEY_PICKED_ITEM = "picked-item"; + + private static final int STATE_INIT = 0; + private static final int STATE_PHOTO_PICKED = 1; + + private int mState = STATE_INIT; + private Uri mPickedItem; + + @Override + protected void onCreate(Bundle bundle) { + super.onCreate(bundle); + if (bundle != null) { + mState = bundle.getInt(KEY_STATE); + mPickedItem = (Uri) bundle.getParcelable(KEY_PICKED_ITEM); + } + } + + @Override + protected void onSaveInstanceState(Bundle saveState) { + saveState.putInt(KEY_STATE, mState); + if (mPickedItem != null) { + saveState.putParcelable(KEY_PICKED_ITEM, mPickedItem); + } + } + + @SuppressWarnings("fallthrough") + @Override + protected void onResume() { + super.onResume(); + Intent intent = getIntent(); + switch (mState) { + case STATE_INIT: { + mPickedItem = intent.getData(); + if (mPickedItem == null) { + Intent request = new Intent(Intent.ACTION_GET_CONTENT) + .setClass(this, DialogPicker.class) + .setType(IMAGE_TYPE); + startActivityForResult(request, STATE_PHOTO_PICKED); + return; + } + mState = STATE_PHOTO_PICKED; + // fall-through + } + case STATE_PHOTO_PICKED: { + int width = getWallpaperDesiredMinimumWidth(); + int height = getWallpaperDesiredMinimumHeight(); + Display display = getWindowManager().getDefaultDisplay(); + float spotlightX = (float) display.getWidth() / width; + float spotlightY = (float) display.getHeight() / height; + Intent request = new Intent(CropImage.ACTION_CROP) + .setDataAndType(mPickedItem, IMAGE_TYPE) + .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT) + .putExtra(CropImage.KEY_OUTPUT_X, width) + .putExtra(CropImage.KEY_OUTPUT_Y, height) + .putExtra(CropImage.KEY_ASPECT_X, width) + .putExtra(CropImage.KEY_ASPECT_Y, height) + .putExtra(CropImage.KEY_SPOTLIGHT_X, spotlightX) + .putExtra(CropImage.KEY_SPOTLIGHT_Y, spotlightY) + .putExtra(CropImage.KEY_SCALE, true) + .putExtra(CropImage.KEY_NO_FACE_DETECTION, true) + .putExtra(CropImage.KEY_SET_AS_WALLPAPER, true); + startActivity(request); + finish(); + } + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode != RESULT_OK) { + setResult(resultCode); + finish(); + return; + } + mState = requestCode; + if (mState == STATE_PHOTO_PICKED) { + mPickedItem = data.getData(); + } + + // onResume() would be called next + } +} diff --git a/src/com/android/gallery3d/data/ChangeNotifier.java b/src/com/android/gallery3d/data/ChangeNotifier.java new file mode 100644 index 000000000..e1e601dd6 --- /dev/null +++ b/src/com/android/gallery3d/data/ChangeNotifier.java @@ -0,0 +1,54 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; + +import android.net.Uri; + +import java.util.concurrent.atomic.AtomicBoolean; + +// This handles change notification for media sets. +public class ChangeNotifier { + + private MediaSet mMediaSet; + private AtomicBoolean mContentDirty = new AtomicBoolean(true); + + public ChangeNotifier(MediaSet set, Uri uri, GalleryApp application) { + mMediaSet = set; + application.getDataManager().registerChangeNotifier(uri, this); + } + + // Returns the dirty flag and clear it. + public boolean isDirty() { + return mContentDirty.compareAndSet(true, false); + } + + public void fakeChange() { + onChange(false); + } + + public void clearDirty() { + mContentDirty.set(false); + } + + protected void onChange(boolean selfChange) { + if (mContentDirty.compareAndSet(false, true)) { + mMediaSet.notifyContentChanged(); + } + } +}
\ No newline at end of file diff --git a/src/com/android/gallery3d/data/ClusterAlbum.java b/src/com/android/gallery3d/data/ClusterAlbum.java new file mode 100644 index 000000000..32f902301 --- /dev/null +++ b/src/com/android/gallery3d/data/ClusterAlbum.java @@ -0,0 +1,129 @@ +/* + * 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.data; + +import java.util.ArrayList; + +public class ClusterAlbum extends MediaSet implements ContentListener { + private static final String TAG = "ClusterAlbum"; + private ArrayList<Path> mPaths = new ArrayList<Path>(); + private String mName = ""; + private DataManager mDataManager; + private MediaSet mClusterAlbumSet; + + public ClusterAlbum(Path path, DataManager dataManager, + MediaSet clusterAlbumSet) { + super(path, nextVersionNumber()); + mDataManager = dataManager; + mClusterAlbumSet = clusterAlbumSet; + mClusterAlbumSet.addContentListener(this); + } + + void setMediaItems(ArrayList<Path> paths) { + mPaths = paths; + } + + ArrayList<Path> getMediaItems() { + return mPaths; + } + + public void setName(String name) { + mName = name; + } + + @Override + public String getName() { + return mName; + } + + @Override + public int getMediaItemCount() { + return mPaths.size(); + } + + @Override + public ArrayList<MediaItem> getMediaItem(int start, int count) { + return getMediaItemFromPath(mPaths, start, count, mDataManager); + } + + public static ArrayList<MediaItem> getMediaItemFromPath( + ArrayList<Path> paths, int start, int count, + DataManager dataManager) { + if (start >= paths.size()) { + return new ArrayList<MediaItem>(); + } + int end = Math.min(start + count, paths.size()); + ArrayList<Path> subset = new ArrayList<Path>(paths.subList(start, end)); + final MediaItem[] buf = new MediaItem[end - start]; + ItemConsumer consumer = new ItemConsumer() { + public void consume(int index, MediaItem item) { + buf[index] = item; + } + }; + dataManager.mapMediaItems(subset, consumer, 0); + ArrayList<MediaItem> result = new ArrayList<MediaItem>(end - start); + for (int i = 0; i < buf.length; i++) { + result.add(buf[i]); + } + return result; + } + + @Override + protected int enumerateMediaItems(ItemConsumer consumer, int startIndex) { + mDataManager.mapMediaItems(mPaths, consumer, startIndex); + return mPaths.size(); + } + + @Override + public int getTotalMediaItemCount() { + return mPaths.size(); + } + + @Override + public long reload() { + if (mClusterAlbumSet.reload() > mDataVersion) { + mDataVersion = nextVersionNumber(); + } + return mDataVersion; + } + + public void onContentDirty() { + notifyContentChanged(); + } + + @Override + public int getSupportedOperations() { + return SUPPORT_SHARE | SUPPORT_DELETE | SUPPORT_INFO; + } + + @Override + public void delete() { + ItemConsumer consumer = new ItemConsumer() { + public void consume(int index, MediaItem item) { + if ((item.getSupportedOperations() & SUPPORT_DELETE) != 0) { + item.delete(); + } + } + }; + mDataManager.mapMediaItems(mPaths, consumer, 0); + } + + @Override + public boolean isLeafAlbum() { + return true; + } +} diff --git a/src/com/android/gallery3d/data/ClusterAlbumSet.java b/src/com/android/gallery3d/data/ClusterAlbumSet.java new file mode 100644 index 000000000..5b0569a67 --- /dev/null +++ b/src/com/android/gallery3d/data/ClusterAlbumSet.java @@ -0,0 +1,152 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; + +import android.content.Context; +import android.net.Uri; + +import java.util.ArrayList; +import java.util.HashSet; + +public class ClusterAlbumSet extends MediaSet implements ContentListener { + private static final String TAG = "ClusterAlbumSet"; + private GalleryApp mApplication; + private MediaSet mBaseSet; + private int mKind; + private ArrayList<ClusterAlbum> mAlbums = new ArrayList<ClusterAlbum>(); + private boolean mFirstReloadDone; + + public ClusterAlbumSet(Path path, GalleryApp application, + MediaSet baseSet, int kind) { + super(path, INVALID_DATA_VERSION); + mApplication = application; + mBaseSet = baseSet; + mKind = kind; + baseSet.addContentListener(this); + } + + @Override + public MediaSet getSubMediaSet(int index) { + return mAlbums.get(index); + } + + @Override + public int getSubMediaSetCount() { + return mAlbums.size(); + } + + @Override + public String getName() { + return mBaseSet.getName(); + } + + @Override + public long reload() { + if (mBaseSet.reload() > mDataVersion) { + if (mFirstReloadDone) { + updateClustersContents(); + } else { + updateClusters(); + mFirstReloadDone = true; + } + mDataVersion = nextVersionNumber(); + } + return mDataVersion; + } + + public void onContentDirty() { + notifyContentChanged(); + } + + private void updateClusters() { + mAlbums.clear(); + Clustering clustering; + Context context = mApplication.getAndroidContext(); + switch (mKind) { + case ClusterSource.CLUSTER_ALBUMSET_TIME: + clustering = new TimeClustering(context); + break; + case ClusterSource.CLUSTER_ALBUMSET_LOCATION: + clustering = new LocationClustering(context); + break; + case ClusterSource.CLUSTER_ALBUMSET_TAG: + clustering = new TagClustering(context); + break; + case ClusterSource.CLUSTER_ALBUMSET_FACE: + clustering = new FaceClustering(context); + break; + default: /* CLUSTER_ALBUMSET_SIZE */ + clustering = new SizeClustering(context); + break; + } + + clustering.run(mBaseSet); + int n = clustering.getNumberOfClusters(); + DataManager dataManager = mApplication.getDataManager(); + for (int i = 0; i < n; i++) { + Path childPath; + String childName = clustering.getClusterName(i); + if (mKind == ClusterSource.CLUSTER_ALBUMSET_TAG) { + childPath = mPath.getChild(Uri.encode(childName)); + } else if (mKind == ClusterSource.CLUSTER_ALBUMSET_SIZE) { + long minSize = ((SizeClustering) clustering).getMinSize(i); + childPath = mPath.getChild(minSize); + } else { + childPath = mPath.getChild(i); + } + ClusterAlbum album = (ClusterAlbum) dataManager.peekMediaObject( + childPath); + if (album == null) { + album = new ClusterAlbum(childPath, dataManager, this); + } + album.setMediaItems(clustering.getCluster(i)); + album.setName(childName); + mAlbums.add(album); + } + } + + private void updateClustersContents() { + final HashSet<Path> existing = new HashSet<Path>(); + mBaseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() { + public void consume(int index, MediaItem item) { + existing.add(item.getPath()); + } + }); + + int n = mAlbums.size(); + + // The loop goes backwards because we may remove empty albums from + // mAlbums. + for (int i = n - 1; i >= 0; i--) { + ArrayList<Path> oldPaths = mAlbums.get(i).getMediaItems(); + ArrayList<Path> newPaths = new ArrayList<Path>(); + int m = oldPaths.size(); + for (int j = 0; j < m; j++) { + Path p = oldPaths.get(j); + if (existing.contains(p)) { + newPaths.add(p); + } + } + mAlbums.get(i).setMediaItems(newPaths); + if (newPaths.isEmpty()) { + mAlbums.remove(i); + } + } + } +} diff --git a/src/com/android/gallery3d/data/ClusterSource.java b/src/com/android/gallery3d/data/ClusterSource.java new file mode 100644 index 000000000..a1f22e57a --- /dev/null +++ b/src/com/android/gallery3d/data/ClusterSource.java @@ -0,0 +1,86 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; + +class ClusterSource extends MediaSource { + static final int CLUSTER_ALBUMSET_TIME = 0; + static final int CLUSTER_ALBUMSET_LOCATION = 1; + static final int CLUSTER_ALBUMSET_TAG = 2; + static final int CLUSTER_ALBUMSET_SIZE = 3; + static final int CLUSTER_ALBUMSET_FACE = 4; + + static final int CLUSTER_ALBUM_TIME = 0x100; + static final int CLUSTER_ALBUM_LOCATION = 0x101; + static final int CLUSTER_ALBUM_TAG = 0x102; + static final int CLUSTER_ALBUM_SIZE = 0x103; + static final int CLUSTER_ALBUM_FACE = 0x104; + + GalleryApp mApplication; + PathMatcher mMatcher; + + public ClusterSource(GalleryApp application) { + super("cluster"); + mApplication = application; + mMatcher = new PathMatcher(); + mMatcher.add("/cluster/*/time", CLUSTER_ALBUMSET_TIME); + mMatcher.add("/cluster/*/location", CLUSTER_ALBUMSET_LOCATION); + mMatcher.add("/cluster/*/tag", CLUSTER_ALBUMSET_TAG); + mMatcher.add("/cluster/*/size", CLUSTER_ALBUMSET_SIZE); + mMatcher.add("/cluster/*/face", CLUSTER_ALBUMSET_FACE); + + mMatcher.add("/cluster/*/time/*", CLUSTER_ALBUM_TIME); + mMatcher.add("/cluster/*/location/*", CLUSTER_ALBUM_LOCATION); + mMatcher.add("/cluster/*/tag/*", CLUSTER_ALBUM_TAG); + mMatcher.add("/cluster/*/size/*", CLUSTER_ALBUM_SIZE); + mMatcher.add("/cluster/*/face/*", CLUSTER_ALBUM_FACE); + } + + // The names we accept are: + // /cluster/{set}/time /cluster/{set}/time/k + // /cluster/{set}/location /cluster/{set}/location/k + // /cluster/{set}/tag /cluster/{set}/tag/encoded_tag + // /cluster/{set}/size /cluster/{set}/size/min_size + @Override + public MediaObject createMediaObject(Path path) { + int matchType = mMatcher.match(path); + String setsName = mMatcher.getVar(0); + DataManager dataManager = mApplication.getDataManager(); + MediaSet[] sets = dataManager.getMediaSetsFromString(setsName); + switch (matchType) { + case CLUSTER_ALBUMSET_TIME: + case CLUSTER_ALBUMSET_LOCATION: + case CLUSTER_ALBUMSET_TAG: + case CLUSTER_ALBUMSET_SIZE: + case CLUSTER_ALBUMSET_FACE: + return new ClusterAlbumSet(path, mApplication, sets[0], matchType); + case CLUSTER_ALBUM_TIME: + case CLUSTER_ALBUM_LOCATION: + case CLUSTER_ALBUM_TAG: + case CLUSTER_ALBUM_SIZE: + case CLUSTER_ALBUM_FACE: { + MediaSet parent = dataManager.getMediaSet(path.getParent()); + // The actual content in the ClusterAlbum will be filled later + // when the reload() method in the parent is run. + return new ClusterAlbum(path, dataManager, parent); + } + default: + throw new RuntimeException("bad path: " + path); + } + } +} diff --git a/src/com/android/gallery3d/data/Clustering.java b/src/com/android/gallery3d/data/Clustering.java new file mode 100644 index 000000000..542dda27f --- /dev/null +++ b/src/com/android/gallery3d/data/Clustering.java @@ -0,0 +1,26 @@ +/* + * 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.data; + +import java.util.ArrayList; + +public abstract class Clustering { + public abstract void run(MediaSet baseSet); + public abstract int getNumberOfClusters(); + public abstract ArrayList<Path> getCluster(int index); + public abstract String getClusterName(int index); +} diff --git a/src/com/android/gallery3d/data/ComboAlbum.java b/src/com/android/gallery3d/data/ComboAlbum.java new file mode 100644 index 000000000..8ca2077a4 --- /dev/null +++ b/src/com/android/gallery3d/data/ComboAlbum.java @@ -0,0 +1,87 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; + +import java.util.ArrayList; + +// ComboAlbum combines multiple media sets into one. It lists all media items +// from the input albums. +// This only handles SubMediaSets, not MediaItems. (That's all we need now) +public class ComboAlbum extends MediaSet implements ContentListener { + private static final String TAG = "ComboAlbum"; + private final MediaSet[] mSets; + private final String mName; + + public ComboAlbum(Path path, MediaSet[] mediaSets, String name) { + super(path, nextVersionNumber()); + mSets = mediaSets; + for (MediaSet set : mSets) { + set.addContentListener(this); + } + mName = name; + } + + @Override + public ArrayList<MediaItem> getMediaItem(int start, int count) { + ArrayList<MediaItem> items = new ArrayList<MediaItem>(); + for (MediaSet set : mSets) { + int size = set.getMediaItemCount(); + if (count < 1) break; + if (start < size) { + int fetchCount = (start + count <= size) ? count : size - start; + ArrayList<MediaItem> fetchItems = set.getMediaItem(start, fetchCount); + items.addAll(fetchItems); + count -= fetchItems.size(); + start = 0; + } else { + start -= size; + } + } + return items; + } + + @Override + public int getMediaItemCount() { + int count = 0; + for (MediaSet set : mSets) { + count += set.getMediaItemCount(); + } + return count; + } + + @Override + public String getName() { + return mName; + } + + @Override + public long reload() { + boolean changed = false; + for (int i = 0, n = mSets.length; i < n; ++i) { + long version = mSets[i].reload(); + if (version > mDataVersion) changed = true; + } + if (changed) mDataVersion = nextVersionNumber(); + return mDataVersion; + } + + public void onContentDirty() { + notifyContentChanged(); + } +} diff --git a/src/com/android/gallery3d/data/ComboAlbumSet.java b/src/com/android/gallery3d/data/ComboAlbumSet.java new file mode 100644 index 000000000..aa196039d --- /dev/null +++ b/src/com/android/gallery3d/data/ComboAlbumSet.java @@ -0,0 +1,80 @@ +/* + * 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.data; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryApp; + +// ComboAlbumSet combines multiple media sets into one. It lists all sub +// media sets from the input album sets. +// This only handles SubMediaSets, not MediaItems. (That's all we need now) +public class ComboAlbumSet extends MediaSet implements ContentListener { + private static final String TAG = "ComboAlbumSet"; + private final MediaSet[] mSets; + private final String mName; + + public ComboAlbumSet(Path path, GalleryApp application, MediaSet[] mediaSets) { + super(path, nextVersionNumber()); + mSets = mediaSets; + for (MediaSet set : mSets) { + set.addContentListener(this); + } + mName = application.getResources().getString( + R.string.set_label_all_albums); + } + + @Override + public MediaSet getSubMediaSet(int index) { + for (MediaSet set : mSets) { + int size = set.getSubMediaSetCount(); + if (index < size) { + return set.getSubMediaSet(index); + } + index -= size; + } + return null; + } + + @Override + public int getSubMediaSetCount() { + int count = 0; + for (MediaSet set : mSets) { + count += set.getSubMediaSetCount(); + } + return count; + } + + @Override + public String getName() { + return mName; + } + + @Override + public long reload() { + boolean changed = false; + for (int i = 0, n = mSets.length; i < n; ++i) { + long version = mSets[i].reload(); + if (version > mDataVersion) changed = true; + } + if (changed) mDataVersion = nextVersionNumber(); + return mDataVersion; + } + + public void onContentDirty() { + notifyContentChanged(); + } +} diff --git a/src/com/android/gallery3d/data/ComboSource.java b/src/com/android/gallery3d/data/ComboSource.java new file mode 100644 index 000000000..867d47e64 --- /dev/null +++ b/src/com/android/gallery3d/data/ComboSource.java @@ -0,0 +1,55 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; + +class ComboSource extends MediaSource { + private static final int COMBO_ALBUMSET = 0; + private static final int COMBO_ALBUM = 1; + private GalleryApp mApplication; + private PathMatcher mMatcher; + + public ComboSource(GalleryApp application) { + super("combo"); + mApplication = application; + mMatcher = new PathMatcher(); + mMatcher.add("/combo/*", COMBO_ALBUMSET); + mMatcher.add("/combo/*/*", COMBO_ALBUM); + } + + // The only path we accept is "/combo/{set1, set2, ...} and /combo/item/{set1, set2, ...}" + @Override + public MediaObject createMediaObject(Path path) { + String[] segments = path.split(); + if (segments.length < 2) { + throw new RuntimeException("bad path: " + path); + } + + DataManager dataManager = mApplication.getDataManager(); + switch (mMatcher.match(path)) { + case COMBO_ALBUMSET: + return new ComboAlbumSet(path, mApplication, + dataManager.getMediaSetsFromString(segments[1])); + + case COMBO_ALBUM: + return new ComboAlbum(path, + dataManager.getMediaSetsFromString(segments[2]), segments[1]); + } + return null; + } +} diff --git a/src/com/android/gallery3d/data/ContentListener.java b/src/com/android/gallery3d/data/ContentListener.java new file mode 100644 index 000000000..5e2952685 --- /dev/null +++ b/src/com/android/gallery3d/data/ContentListener.java @@ -0,0 +1,21 @@ +/* + * 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.data; + +public interface ContentListener { + public void onContentDirty(); +}
\ No newline at end of file diff --git a/src/com/android/gallery3d/data/DataManager.java b/src/com/android/gallery3d/data/DataManager.java new file mode 100644 index 000000000..f7dac5ebd --- /dev/null +++ b/src/com/android/gallery3d/data/DataManager.java @@ -0,0 +1,333 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.MediaSet.ItemConsumer; +import com.android.gallery3d.data.MediaSource.PathId; +import com.android.gallery3d.picasasource.PicasaSource; + +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map.Entry; +import java.util.WeakHashMap; + +// DataManager manages all media sets and media items in the system. +// +// Each MediaSet and MediaItem has a unique 64 bits id. The most significant +// 32 bits represents its parent, and the least significant 32 bits represents +// the self id. For MediaSet the self id is is globally unique, but for +// MediaItem it's unique only relative to its parent. +// +// To make sure the id is the same when the MediaSet is re-created, a child key +// is provided to obtainSetId() to make sure the same self id will be used as +// when the parent and key are the same. A sequence of child keys is called a +// path. And it's used to identify a specific media set even if the process is +// killed and re-created, so child keys should be stable identifiers. + +public class DataManager { + public static final int INCLUDE_IMAGE = 1; + public static final int INCLUDE_VIDEO = 2; + public static final int INCLUDE_ALL = INCLUDE_IMAGE | INCLUDE_VIDEO; + public static final int INCLUDE_LOCAL_ONLY = 4; + public static final int INCLUDE_LOCAL_IMAGE_ONLY = + INCLUDE_LOCAL_ONLY | INCLUDE_IMAGE; + public static final int INCLUDE_LOCAL_VIDEO_ONLY = + INCLUDE_LOCAL_ONLY | INCLUDE_VIDEO; + public static final int INCLUDE_LOCAL_ALL_ONLY = + INCLUDE_LOCAL_ONLY | INCLUDE_IMAGE | INCLUDE_VIDEO; + + // Any one who would like to access data should require this lock + // to prevent concurrency issue. + public static final Object LOCK = new Object(); + + private static final String TAG = "DataManager"; + + // This is the path for the media set seen by the user at top level. + private static final String TOP_SET_PATH = + "/combo/{/mtp,/local/all,/picasa/all}"; + private static final String TOP_IMAGE_SET_PATH = + "/combo/{/mtp,/local/image,/picasa/image}"; + private static final String TOP_VIDEO_SET_PATH = + "/combo/{/local/video,/picasa/video}"; + private static final String TOP_LOCAL_SET_PATH = + "/local/all"; + private static final String TOP_LOCAL_IMAGE_SET_PATH = + "/local/image"; + private static final String TOP_LOCAL_VIDEO_SET_PATH = + "/local/video"; + + public static final Comparator<MediaItem> sDateTakenComparator = + new DateTakenComparator(); + + private static class DateTakenComparator implements Comparator<MediaItem> { + public int compare(MediaItem item1, MediaItem item2) { + return -Utils.compare(item1.getDateInMs(), item2.getDateInMs()); + } + } + + private final Handler mDefaultMainHandler; + + private GalleryApp mApplication; + private int mActiveCount = 0; + + private HashMap<Uri, NotifyBroker> mNotifierMap = + new HashMap<Uri, NotifyBroker>(); + + + private HashMap<String, MediaSource> mSourceMap = + new LinkedHashMap<String, MediaSource>(); + + public DataManager(GalleryApp application) { + mApplication = application; + mDefaultMainHandler = new Handler(application.getMainLooper()); + } + + public synchronized void initializeSourceMap() { + if (!mSourceMap.isEmpty()) return; + + // the order matters, the UriSource must come last + addSource(new LocalSource(mApplication)); + addSource(new PicasaSource(mApplication)); + addSource(new MtpSource(mApplication)); + addSource(new ComboSource(mApplication)); + addSource(new ClusterSource(mApplication)); + addSource(new FilterSource(mApplication)); + addSource(new UriSource(mApplication)); + + if (mActiveCount > 0) { + for (MediaSource source : mSourceMap.values()) { + source.resume(); + } + } + } + + public String getTopSetPath(int typeBits) { + + switch (typeBits) { + case INCLUDE_IMAGE: return TOP_IMAGE_SET_PATH; + case INCLUDE_VIDEO: return TOP_VIDEO_SET_PATH; + case INCLUDE_ALL: return TOP_SET_PATH; + case INCLUDE_LOCAL_IMAGE_ONLY: return TOP_LOCAL_IMAGE_SET_PATH; + case INCLUDE_LOCAL_VIDEO_ONLY: return TOP_LOCAL_VIDEO_SET_PATH; + case INCLUDE_LOCAL_ALL_ONLY: return TOP_LOCAL_SET_PATH; + default: throw new IllegalArgumentException(); + } + } + + // open for debug + void addSource(MediaSource source) { + mSourceMap.put(source.getPrefix(), source); + } + + public MediaObject peekMediaObject(Path path) { + return path.getObject(); + } + + public MediaSet peekMediaSet(Path path) { + return (MediaSet) path.getObject(); + } + + public MediaObject getMediaObject(Path path) { + MediaObject obj = path.getObject(); + if (obj != null) return obj; + + MediaSource source = mSourceMap.get(path.getPrefix()); + if (source == null) { + Log.w(TAG, "cannot find media source for path: " + path); + return null; + } + + MediaObject object = source.createMediaObject(path); + if (object == null) { + Log.w(TAG, "cannot create media object: " + path); + } + return object; + } + + public MediaObject getMediaObject(String s) { + return getMediaObject(Path.fromString(s)); + } + + public MediaSet getMediaSet(Path path) { + return (MediaSet) getMediaObject(path); + } + + public MediaSet getMediaSet(String s) { + return (MediaSet) getMediaObject(s); + } + + public MediaSet[] getMediaSetsFromString(String segment) { + String[] seq = Path.splitSequence(segment); + int n = seq.length; + MediaSet[] sets = new MediaSet[n]; + for (int i = 0; i < n; i++) { + sets[i] = getMediaSet(seq[i]); + } + return sets; + } + + // Maps a list of Paths to MediaItems, and invoke consumer.consume() + // for each MediaItem (may not be in the same order as the input list). + // An index number is also passed to consumer.consume() to identify + // the original position in the input list of the corresponding Path (plus + // startIndex). + public void mapMediaItems(ArrayList<Path> list, ItemConsumer consumer, + int startIndex) { + HashMap<String, ArrayList<PathId>> map = + new HashMap<String, ArrayList<PathId>>(); + + // Group the path by the prefix. + int n = list.size(); + for (int i = 0; i < n; i++) { + Path path = list.get(i); + String prefix = path.getPrefix(); + ArrayList<PathId> group = map.get(prefix); + if (group == null) { + group = new ArrayList<PathId>(); + map.put(prefix, group); + } + group.add(new PathId(path, i + startIndex)); + } + + // For each group, ask the corresponding media source to map it. + for (Entry<String, ArrayList<PathId>> entry : map.entrySet()) { + String prefix = entry.getKey(); + MediaSource source = mSourceMap.get(prefix); + source.mapMediaItems(entry.getValue(), consumer); + } + } + + // The following methods forward the request to the proper object. + public int getSupportedOperations(Path path) { + return getMediaObject(path).getSupportedOperations(); + } + + public void delete(Path path) { + getMediaObject(path).delete(); + } + + public void rotate(Path path, int degrees) { + getMediaObject(path).rotate(degrees); + } + + public Uri getContentUri(Path path) { + return getMediaObject(path).getContentUri(); + } + + public int getMediaType(Path path) { + return getMediaObject(path).getMediaType(); + } + + public MediaDetails getDetails(Path path) { + return getMediaObject(path).getDetails(); + } + + public void cache(Path path, int flag) { + getMediaObject(path).cache(flag); + } + + public Path findPathByUri(Uri uri) { + if (uri == null) return null; + for (MediaSource source : mSourceMap.values()) { + Path path = source.findPathByUri(uri); + if (path != null) return path; + } + return null; + } + + public Path getDefaultSetOf(Path item) { + MediaSource source = mSourceMap.get(item.getPrefix()); + return source == null ? null : source.getDefaultSetOf(item); + } + + // Returns number of bytes used by cached pictures currently downloaded. + public long getTotalUsedCacheSize() { + long sum = 0; + for (MediaSource source : mSourceMap.values()) { + sum += source.getTotalUsedCacheSize(); + } + return sum; + } + + // Returns number of bytes used by cached pictures if all pending + // downloads and removals are completed. + public long getTotalTargetCacheSize() { + long sum = 0; + for (MediaSource source : mSourceMap.values()) { + sum += source.getTotalTargetCacheSize(); + } + return sum; + } + + public void registerChangeNotifier(Uri uri, ChangeNotifier notifier) { + NotifyBroker broker = null; + synchronized (mNotifierMap) { + broker = mNotifierMap.get(uri); + if (broker == null) { + broker = new NotifyBroker(mDefaultMainHandler); + mApplication.getContentResolver() + .registerContentObserver(uri, true, broker); + mNotifierMap.put(uri, broker); + } + } + broker.registerNotifier(notifier); + } + + public void resume() { + if (++mActiveCount == 1) { + for (MediaSource source : mSourceMap.values()) { + source.resume(); + } + } + } + + public void pause() { + if (--mActiveCount == 0) { + for (MediaSource source : mSourceMap.values()) { + source.pause(); + } + } + } + + private static class NotifyBroker extends ContentObserver { + private WeakHashMap<ChangeNotifier, Object> mNotifiers = + new WeakHashMap<ChangeNotifier, Object>(); + + public NotifyBroker(Handler handler) { + super(handler); + } + + public synchronized void registerNotifier(ChangeNotifier notifier) { + mNotifiers.put(notifier, null); + } + + @Override + public synchronized void onChange(boolean selfChange) { + for(ChangeNotifier notifier : mNotifiers.keySet()) { + notifier.onChange(selfChange); + } + } + } +} diff --git a/src/com/android/gallery3d/data/DecodeUtils.java b/src/com/android/gallery3d/data/DecodeUtils.java new file mode 100644 index 000000000..e7ae638c2 --- /dev/null +++ b/src/com/android/gallery3d/data/DecodeUtils.java @@ -0,0 +1,173 @@ +/* + * 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.data; + +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.ThreadPool.CancelListener; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.content.ContentResolver; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.graphics.BitmapFactory.Options; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Rect; +import android.net.Uri; +import android.os.ParcelFileDescriptor; + +import java.io.FileDescriptor; +import java.io.FileInputStream; + +public class DecodeUtils { + private static final String TAG = "DecodeService"; + + private static class DecodeCanceller implements CancelListener { + Options mOptions; + public DecodeCanceller(Options options) { + mOptions = options; + } + public void onCancel() { + mOptions.requestCancelDecode(); + } + } + + public static Bitmap requestDecode(JobContext jc, final String filePath, + Options options) { + if (options == null) options = new Options(); + jc.setCancelListener(new DecodeCanceller(options)); + return ensureGLCompatibleBitmap( + BitmapFactory.decodeFile(filePath, options)); + } + + public static Bitmap requestDecode(JobContext jc, byte[] bytes, + Options options) { + return requestDecode(jc, bytes, 0, bytes.length, options); + } + + public static Bitmap requestDecode(JobContext jc, byte[] bytes, int offset, + int length, Options options) { + if (options == null) options = new Options(); + jc.setCancelListener(new DecodeCanceller(options)); + return ensureGLCompatibleBitmap( + BitmapFactory.decodeByteArray(bytes, offset, length, options)); + } + + public static Bitmap requestDecode(JobContext jc, final String filePath, + Options options, int targetSize) { + FileInputStream fis = null; + try { + fis = new FileInputStream(filePath); + FileDescriptor fd = fis.getFD(); + return requestDecode(jc, fd, options, targetSize); + } catch (Exception ex) { + Log.w(TAG, ex); + return null; + } finally { + Utils.closeSilently(fis); + } + } + + public static Bitmap requestDecode(JobContext jc, FileDescriptor fd, + Options options, int targetSize) { + if (options == null) options = new Options(); + jc.setCancelListener(new DecodeCanceller(options)); + + options.inJustDecodeBounds = true; + BitmapFactory.decodeFileDescriptor(fd, null, options); + if (jc.isCancelled()) return null; + + options.inSampleSize = BitmapUtils.computeSampleSizeLarger( + options.outWidth, options.outHeight, targetSize); + options.inJustDecodeBounds = false; + return ensureGLCompatibleBitmap( + BitmapFactory.decodeFileDescriptor(fd, null, options)); + } + + public static Bitmap requestDecode(JobContext jc, + FileDescriptor fileDescriptor, Rect paddings, Options options) { + if (options == null) options = new Options(); + jc.setCancelListener(new DecodeCanceller(options)); + return ensureGLCompatibleBitmap(BitmapFactory.decodeFileDescriptor + (fileDescriptor, paddings, options)); + } + + // TODO: This function should not be called directly from + // DecodeUtils.requestDecode(...), since we don't have the knowledge + // if the bitmap will be uploaded to GL. + public static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) { + if (bitmap == null || bitmap.getConfig() != null) return bitmap; + Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false); + bitmap.recycle(); + return newBitmap; + } + + public static BitmapRegionDecoder requestCreateBitmapRegionDecoder( + JobContext jc, byte[] bytes, int offset, int length, + boolean shareable) { + if (offset < 0 || length <= 0 || offset + length > bytes.length) { + throw new IllegalArgumentException(String.format( + "offset = %s, length = %s, bytes = %s", + offset, length, bytes.length)); + } + + try { + return BitmapRegionDecoder.newInstance( + bytes, offset, length, shareable); + } catch (Throwable t) { + Log.w(TAG, t); + return null; + } + } + + public static BitmapRegionDecoder requestCreateBitmapRegionDecoder( + JobContext jc, String filePath, boolean shareable) { + try { + return BitmapRegionDecoder.newInstance(filePath, shareable); + } catch (Throwable t) { + Log.w(TAG, t); + return null; + } + } + + public static BitmapRegionDecoder requestCreateBitmapRegionDecoder( + JobContext jc, FileDescriptor fd, boolean shareable) { + try { + return BitmapRegionDecoder.newInstance(fd, shareable); + } catch (Throwable t) { + Log.w(TAG, t); + return null; + } + } + + public static BitmapRegionDecoder requestCreateBitmapRegionDecoder( + JobContext jc, Uri uri, ContentResolver resolver, + boolean shareable) { + ParcelFileDescriptor pfd = null; + try { + pfd = resolver.openFileDescriptor(uri, "r"); + return BitmapRegionDecoder.newInstance( + pfd.getFileDescriptor(), shareable); + } catch (Throwable t) { + Log.w(TAG, t); + return null; + } finally { + Utils.closeSilently(pfd); + } + } +} diff --git a/src/com/android/gallery3d/data/DownloadCache.java b/src/com/android/gallery3d/data/DownloadCache.java new file mode 100644 index 000000000..30ba668c3 --- /dev/null +++ b/src/com/android/gallery3d/data/DownloadCache.java @@ -0,0 +1,398 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.LruCache; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DownloadEntry.Columns; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.ThreadPool; +import com.android.gallery3d.util.ThreadPool.CancelListener; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import java.io.File; +import java.net.URL; +import java.util.HashMap; +import java.util.HashSet; +import java.util.WeakHashMap; + +public class DownloadCache { + private static final String TAG = "DownloadCache"; + private static final int MAX_DELETE_COUNT = 16; + private static final int LRU_CAPACITY = 4; + + private static final String TABLE_NAME = DownloadEntry.SCHEMA.getTableName(); + + private static final String QUERY_PROJECTION[] = {Columns.ID, Columns.DATA}; + private static final String WHERE_HASH_AND_URL = String.format( + "%s = ? AND %s = ?", Columns.HASH_CODE, Columns.CONTENT_URL); + private static final int QUERY_INDEX_ID = 0; + private static final int QUERY_INDEX_DATA = 1; + + private static final String FREESPACE_PROJECTION[] = { + Columns.ID, Columns.DATA, Columns.CONTENT_URL, Columns.CONTENT_SIZE}; + private static final String FREESPACE_ORDER_BY = + String.format("%s ASC", Columns.LAST_ACCESS); + private static final int FREESPACE_IDNEX_ID = 0; + private static final int FREESPACE_IDNEX_DATA = 1; + private static final int FREESPACE_INDEX_CONTENT_URL = 2; + private static final int FREESPACE_INDEX_CONTENT_SIZE = 3; + + private static final String ID_WHERE = Columns.ID + " = ?"; + + private static final String SUM_PROJECTION[] = + {String.format("sum(%s)", Columns.CONTENT_SIZE)}; + private static final int SUM_INDEX_SUM = 0; + + private final LruCache<String, Entry> mEntryMap = + new LruCache<String, Entry>(LRU_CAPACITY); + private final HashMap<String, DownloadTask> mTaskMap = + new HashMap<String, DownloadTask>(); + private final File mRoot; + private final GalleryApp mApplication; + private final SQLiteDatabase mDatabase; + private final long mCapacity; + + private long mTotalBytes = 0; + private boolean mInitialized = false; + private WeakHashMap<Object, Entry> mAssociateMap = new WeakHashMap<Object, Entry>(); + + public DownloadCache(GalleryApp application, File root, long capacity) { + mRoot = Utils.checkNotNull(root); + mApplication = Utils.checkNotNull(application); + mCapacity = capacity; + mDatabase = new DatabaseHelper(application.getAndroidContext()) + .getWritableDatabase(); + } + + private Entry findEntryInDatabase(String stringUrl) { + long hash = Utils.crc64Long(stringUrl); + String whereArgs[] = {String.valueOf(hash), stringUrl}; + Cursor cursor = mDatabase.query(TABLE_NAME, QUERY_PROJECTION, + WHERE_HASH_AND_URL, whereArgs, null, null, null); + try { + if (cursor.moveToNext()) { + File file = new File(cursor.getString(QUERY_INDEX_DATA)); + long id = cursor.getInt(QUERY_INDEX_ID); + Entry entry = null; + synchronized (mEntryMap) { + entry = mEntryMap.get(stringUrl); + if (entry == null) { + entry = new Entry(id, file); + mEntryMap.put(stringUrl, entry); + } + } + return entry; + } + } finally { + cursor.close(); + } + return null; + } + + public Entry lookup(URL url) { + if (!mInitialized) initialize(); + String stringUrl = url.toString(); + + // First find in the entry-pool + synchronized (mEntryMap) { + Entry entry = mEntryMap.get(stringUrl); + if (entry != null) { + updateLastAccess(entry.mId); + return entry; + } + } + + // Then, find it in database + TaskProxy proxy = new TaskProxy(); + synchronized (mTaskMap) { + Entry entry = findEntryInDatabase(stringUrl); + if (entry != null) { + updateLastAccess(entry.mId); + return entry; + } + } + return null; + } + + public Entry download(JobContext jc, URL url) { + if (!mInitialized) initialize(); + + String stringUrl = url.toString(); + + // First find in the entry-pool + synchronized (mEntryMap) { + Entry entry = mEntryMap.get(stringUrl); + if (entry != null) { + updateLastAccess(entry.mId); + return entry; + } + } + + // Then, find it in database + TaskProxy proxy = new TaskProxy(); + synchronized (mTaskMap) { + Entry entry = findEntryInDatabase(stringUrl); + if (entry != null) { + updateLastAccess(entry.mId); + return entry; + } + + // Finally, we need to download the file .... + // First check if we are downloading it now ... + DownloadTask task = mTaskMap.get(stringUrl); + if (task == null) { // if not, start the download task now + task = new DownloadTask(stringUrl); + mTaskMap.put(stringUrl, task); + task.mFuture = mApplication.getThreadPool().submit(task, task); + } + task.addProxy(proxy); + } + + return proxy.get(jc); + } + + private void updateLastAccess(long id) { + ContentValues values = new ContentValues(); + values.put(Columns.LAST_ACCESS, System.currentTimeMillis()); + mDatabase.update(TABLE_NAME, values, + ID_WHERE, new String[] {String.valueOf(id)}); + } + + private synchronized void freeSomeSpaceIfNeed(int maxDeleteFileCount) { + if (mTotalBytes <= mCapacity) return; + Cursor cursor = mDatabase.query(TABLE_NAME, + FREESPACE_PROJECTION, null, null, null, null, FREESPACE_ORDER_BY); + try { + while (maxDeleteFileCount > 0 + && mTotalBytes > mCapacity && cursor.moveToNext()) { + long id = cursor.getLong(FREESPACE_IDNEX_ID); + String url = cursor.getString(FREESPACE_INDEX_CONTENT_URL); + long size = cursor.getLong(FREESPACE_INDEX_CONTENT_SIZE); + String path = cursor.getString(FREESPACE_IDNEX_DATA); + boolean containsKey; + synchronized (mEntryMap) { + containsKey = mEntryMap.containsKey(url); + } + if (!containsKey) { + --maxDeleteFileCount; + mTotalBytes -= size; + new File(path).delete(); + mDatabase.delete(TABLE_NAME, + ID_WHERE, new String[]{String.valueOf(id)}); + } else { + // skip delete, since it is being used + } + } + } finally { + cursor.close(); + } + } + + private synchronized long insertEntry(String url, File file) { + long size = file.length(); + mTotalBytes += size; + + ContentValues values = new ContentValues(); + String hashCode = String.valueOf(Utils.crc64Long(url)); + values.put(Columns.DATA, file.getAbsolutePath()); + values.put(Columns.HASH_CODE, hashCode); + values.put(Columns.CONTENT_URL, url); + values.put(Columns.CONTENT_SIZE, size); + values.put(Columns.LAST_UPDATED, System.currentTimeMillis()); + return mDatabase.insert(TABLE_NAME, "", values); + } + + private synchronized void initialize() { + if (mInitialized) return; + mInitialized = true; + if (!mRoot.isDirectory()) mRoot.mkdirs(); + if (!mRoot.isDirectory()) { + throw new RuntimeException("cannot create " + mRoot.getAbsolutePath()); + } + + Cursor cursor = mDatabase.query( + TABLE_NAME, SUM_PROJECTION, null, null, null, null, null); + mTotalBytes = 0; + try { + if (cursor.moveToNext()) { + mTotalBytes = cursor.getLong(SUM_INDEX_SUM); + } + } finally { + cursor.close(); + } + if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT); + } + + private final class DatabaseHelper extends SQLiteOpenHelper { + public static final String DATABASE_NAME = "download.db"; + public static final int DATABASE_VERSION = 2; + + public DatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + DownloadEntry.SCHEMA.createTables(db); + // Delete old files + for (File file : mRoot.listFiles()) { + if (!file.delete()) { + Log.w(TAG, "fail to remove: " + file.getAbsolutePath()); + } + } + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + //reset everything + DownloadEntry.SCHEMA.dropTables(db); + onCreate(db); + } + } + + public class Entry { + public File cacheFile; + protected long mId; + + Entry(long id, File cacheFile) { + mId = id; + this.cacheFile = Utils.checkNotNull(cacheFile); + } + + public void associateWith(Object object) { + mAssociateMap.put(Utils.checkNotNull(object), this); + } + } + + private class DownloadTask implements Job<File>, FutureListener<File> { + private HashSet<TaskProxy> mProxySet = new HashSet<TaskProxy>(); + private Future<File> mFuture; + private final String mUrl; + + public DownloadTask(String url) { + mUrl = Utils.checkNotNull(url); + } + + public void removeProxy(TaskProxy proxy) { + synchronized (mTaskMap) { + Utils.assertTrue(mProxySet.remove(proxy)); + if (mProxySet.isEmpty()) { + mFuture.cancel(); + mTaskMap.remove(mUrl); + } + } + } + + // should be used in synchronized block of mDatabase + public void addProxy(TaskProxy proxy) { + proxy.mTask = this; + mProxySet.add(proxy); + } + + public void onFutureDone(Future<File> future) { + File file = future.get(); + long id = 0; + if (file != null) { // insert to database + id = insertEntry(mUrl, file); + } + + if (future.isCancelled()) { + Utils.assertTrue(mProxySet.isEmpty()); + return; + } + + synchronized (mTaskMap) { + Entry entry = null; + synchronized (mEntryMap) { + if (file != null) { + entry = new Entry(id, file); + Utils.assertTrue(mEntryMap.put(mUrl, entry) == null); + } + } + for (TaskProxy proxy : mProxySet) { + proxy.setResult(entry); + } + mTaskMap.remove(mUrl); + freeSomeSpaceIfNeed(MAX_DELETE_COUNT); + } + } + + public File run(JobContext jc) { + // TODO: utilize etag + jc.setMode(ThreadPool.MODE_NETWORK); + File tempFile = null; + try { + URL url = new URL(mUrl); + tempFile = File.createTempFile("cache", ".tmp", mRoot); + // download from url to tempFile + jc.setMode(ThreadPool.MODE_NETWORK); + boolean downloaded = DownloadUtils.requestDownload(jc, url, tempFile); + jc.setMode(ThreadPool.MODE_NONE); + if (downloaded) return tempFile; + } catch (Exception e) { + Log.e(TAG, String.format("fail to download %s", mUrl), e); + } finally { + jc.setMode(ThreadPool.MODE_NONE); + } + if (tempFile != null) tempFile.delete(); + return null; + } + } + + public static class TaskProxy { + private DownloadTask mTask; + private boolean mIsCancelled = false; + private Entry mEntry; + + synchronized void setResult(Entry entry) { + if (mIsCancelled) return; + mEntry = entry; + notifyAll(); + } + + public synchronized Entry get(JobContext jc) { + jc.setCancelListener(new CancelListener() { + public void onCancel() { + mTask.removeProxy(TaskProxy.this); + synchronized (TaskProxy.this) { + mIsCancelled = true; + TaskProxy.this.notifyAll(); + } + } + }); + while (!mIsCancelled && mEntry == null) { + try { + wait(); + } catch (InterruptedException e) { + Log.w(TAG, "ignore interrupt", e); + } + } + jc.setCancelListener(null); + return mEntry; + } + } +} diff --git a/src/com/android/gallery3d/data/DownloadEntry.java b/src/com/android/gallery3d/data/DownloadEntry.java new file mode 100644 index 000000000..578523f73 --- /dev/null +++ b/src/com/android/gallery3d/data/DownloadEntry.java @@ -0,0 +1,72 @@ +/* + * 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.data; + +import com.android.gallery3d.common.Entry; +import com.android.gallery3d.common.EntrySchema; + + +@Entry.Table("download") +public class DownloadEntry extends Entry { + public static final EntrySchema SCHEMA = new EntrySchema(DownloadEntry.class); + + public static interface Columns extends Entry.Columns { + public static final String HASH_CODE = "hash_code"; + public static final String CONTENT_URL = "content_url"; + public static final String CONTENT_SIZE = "_size"; + public static final String ETAG = "etag"; + public static final String LAST_ACCESS = "last_access"; + public static final String LAST_UPDATED = "last_updated"; + public static final String DATA = "_data"; + } + + @Column(value = "hash_code", indexed = true) + public long hashCode; + + @Column("content_url") + public String contentUrl; + + @Column("_size") + public long contentSize; + + @Column("etag") + public String eTag; + + @Column(value = "last_access", indexed = true) + public long lastAccessTime; + + @Column(value = "last_updated") + public long lastUpdatedTime; + + @Column("_data") + public String path; + + @Override + public String toString() { + // Note: THIS IS REQUIRED. We used all the fields here. Otherwise, + // ProGuard will remove these UNUSED fields. However, these + // fields are needed to generate database. + return new StringBuilder() + .append("hash_code: ").append(hashCode).append(", ") + .append("content_url").append(contentUrl).append(", ") + .append("_size").append(contentSize).append(", ") + .append("etag").append(eTag).append(", ") + .append("last_access").append(lastAccessTime).append(", ") + .append("last_updated").append(lastUpdatedTime).append(",") + .append("_data").append(path) + .toString(); + } +} diff --git a/src/com/android/gallery3d/data/DownloadUtils.java b/src/com/android/gallery3d/data/DownloadUtils.java new file mode 100644 index 000000000..9632db984 --- /dev/null +++ b/src/com/android/gallery3d/data/DownloadUtils.java @@ -0,0 +1,95 @@ +/* + * 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.data; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.ThreadPool.CancelListener; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.net.URL; + +public class DownloadUtils { + private static final String TAG = "DownloadService"; + + public static boolean requestDownload(JobContext jc, URL url, File file) { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(file); + return download(jc, url, fos); + } catch (Throwable t) { + return false; + } finally { + Utils.closeSilently(fos); + } + } + + public static byte[] requestDownload(JobContext jc, URL url) { + ByteArrayOutputStream baos = null; + try { + baos = new ByteArrayOutputStream(); + if (!download(jc, url, baos)) { + return null; + } + return baos.toByteArray(); + } catch (Throwable t) { + Log.w(TAG, t); + return null; + } finally { + Utils.closeSilently(baos); + } + } + + public static void dump(JobContext jc, InputStream is, OutputStream os) + throws IOException { + byte buffer[] = new byte[4096]; + int rc = is.read(buffer, 0, buffer.length); + final Thread thread = Thread.currentThread(); + jc.setCancelListener(new CancelListener() { + public void onCancel() { + thread.interrupt(); + } + }); + while (rc > 0) { + if (jc.isCancelled()) throw new InterruptedIOException(); + os.write(buffer, 0, rc); + rc = is.read(buffer, 0, buffer.length); + } + jc.setCancelListener(null); + Thread.interrupted(); // consume the interrupt signal + } + + public static boolean download(JobContext jc, URL url, OutputStream output) { + InputStream input = null; + try { + input = url.openStream(); + dump(jc, input, output); + return true; + } catch (Throwable t) { + Log.w(TAG, "fail to download", t); + return false; + } finally { + Utils.closeSilently(input); + } + } +}
\ No newline at end of file diff --git a/src/com/android/gallery3d/data/Face.java b/src/com/android/gallery3d/data/Face.java new file mode 100644 index 000000000..cc1a2d3dc --- /dev/null +++ b/src/com/android/gallery3d/data/Face.java @@ -0,0 +1,56 @@ +/* + * 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.data; + +import com.android.gallery3d.common.Utils; + +public class Face implements Comparable<Face> { + private String mName; + private String mPersonId; + + public Face(String name, String personId) { + mName = name; + mPersonId = personId; + Utils.assertTrue(mName != null && mPersonId != null); + } + + public String getName() { + return mName; + } + + public String getPersonId() { + return mPersonId; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Face) { + Face face = (Face) obj; + return mPersonId.equals(face.mPersonId); + } + return false; + } + + @Override + public int hashCode() { + return mPersonId.hashCode(); + } + + public int compareTo(Face another) { + return mPersonId.compareTo(another.mPersonId); + } +} diff --git a/src/com/android/gallery3d/data/FaceClustering.java b/src/com/android/gallery3d/data/FaceClustering.java new file mode 100644 index 000000000..6ed73b560 --- /dev/null +++ b/src/com/android/gallery3d/data/FaceClustering.java @@ -0,0 +1,94 @@ +/* + * 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.data; + +import com.android.gallery3d.R; + +import android.content.Context; + +import java.util.ArrayList; +import java.util.Map; +import java.util.TreeMap; + +public class FaceClustering extends Clustering { + @SuppressWarnings("unused") + private static final String TAG = "FaceClustering"; + + private ArrayList<ArrayList<Path>> mClusters; + private String[] mNames; + private String mUntaggedString; + + public FaceClustering(Context context) { + mUntaggedString = context.getResources().getString(R.string.untagged); + } + + @Override + public void run(MediaSet baseSet) { + final TreeMap<Face, ArrayList<Path>> map = + new TreeMap<Face, ArrayList<Path>>(); + final ArrayList<Path> untagged = new ArrayList<Path>(); + + baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() { + public void consume(int index, MediaItem item) { + Path path = item.getPath(); + + Face[] faces = item.getFaces(); + if (faces == null || faces.length == 0) { + untagged.add(path); + return; + } + for (int j = 0; j < faces.length; j++) { + Face key = faces[j]; + ArrayList<Path> list = map.get(key); + if (list == null) { + list = new ArrayList<Path>(); + map.put(key, list); + } + list.add(path); + } + } + }); + + int m = map.size(); + mClusters = new ArrayList<ArrayList<Path>>(); + mNames = new String[m + ((untagged.size() > 0) ? 1 : 0)]; + int i = 0; + for (Map.Entry<Face, ArrayList<Path>> entry : map.entrySet()) { + mNames[i++] = entry.getKey().getName(); + mClusters.add(entry.getValue()); + } + if (untagged.size() > 0) { + mNames[i++] = mUntaggedString; + mClusters.add(untagged); + } + } + + @Override + public int getNumberOfClusters() { + return mClusters.size(); + } + + @Override + public ArrayList<Path> getCluster(int index) { + return mClusters.get(index); + } + + @Override + public String getClusterName(int index) { + return mNames[index]; + } +} diff --git a/src/com/android/gallery3d/data/FilterSet.java b/src/com/android/gallery3d/data/FilterSet.java new file mode 100644 index 000000000..9cb7e02ef --- /dev/null +++ b/src/com/android/gallery3d/data/FilterSet.java @@ -0,0 +1,137 @@ +/* + * 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.data; + +import java.util.ArrayList; + +// FilterSet filters a base MediaSet according to a condition. Currently the +// condition is a matching media type. It can be extended to other conditions +// if needed. +public class FilterSet extends MediaSet implements ContentListener { + @SuppressWarnings("unused") + private static final String TAG = "FilterSet"; + + private final DataManager mDataManager; + private final MediaSet mBaseSet; + private final int mMediaType; + private final ArrayList<Path> mPaths = new ArrayList<Path>(); + private final ArrayList<MediaSet> mAlbums = new ArrayList<MediaSet>(); + + public FilterSet(Path path, DataManager dataManager, MediaSet baseSet, + int mediaType) { + super(path, INVALID_DATA_VERSION); + mDataManager = dataManager; + mBaseSet = baseSet; + mMediaType = mediaType; + mBaseSet.addContentListener(this); + } + + @Override + public String getName() { + return mBaseSet.getName(); + } + + @Override + public MediaSet getSubMediaSet(int index) { + return mAlbums.get(index); + } + + @Override + public int getSubMediaSetCount() { + return mAlbums.size(); + } + + @Override + public int getMediaItemCount() { + return mPaths.size(); + } + + @Override + public ArrayList<MediaItem> getMediaItem(int start, int count) { + return ClusterAlbum.getMediaItemFromPath( + mPaths, start, count, mDataManager); + } + + @Override + public long reload() { + if (mBaseSet.reload() > mDataVersion) { + updateData(); + mDataVersion = nextVersionNumber(); + } + return mDataVersion; + } + + @Override + public void onContentDirty() { + notifyContentChanged(); + } + + private void updateData() { + // Albums + mAlbums.clear(); + String basePath = "/filter/mediatype/" + mMediaType; + + for (int i = 0, n = mBaseSet.getSubMediaSetCount(); i < n; i++) { + MediaSet set = mBaseSet.getSubMediaSet(i); + String filteredPath = basePath + "/{" + set.getPath().toString() + "}"; + MediaSet filteredSet = mDataManager.getMediaSet(filteredPath); + filteredSet.reload(); + if (filteredSet.getMediaItemCount() > 0 + || filteredSet.getSubMediaSetCount() > 0) { + mAlbums.add(filteredSet); + } + } + + // Items + mPaths.clear(); + final int total = mBaseSet.getMediaItemCount(); + final Path[] buf = new Path[total]; + + mBaseSet.enumerateMediaItems(new MediaSet.ItemConsumer() { + public void consume(int index, MediaItem item) { + if (item.getMediaType() == mMediaType) { + if (index < 0 || index >= total) return; + Path path = item.getPath(); + buf[index] = path; + } + } + }); + + for (int i = 0; i < total; i++) { + if (buf[i] != null) { + mPaths.add(buf[i]); + } + } + } + + @Override + public int getSupportedOperations() { + return SUPPORT_SHARE | SUPPORT_DELETE; + } + + @Override + public void delete() { + ItemConsumer consumer = new ItemConsumer() { + public void consume(int index, MediaItem item) { + if ((item.getSupportedOperations() & SUPPORT_DELETE) != 0) { + item.delete(); + } + } + }; + mDataManager.mapMediaItems(mPaths, consumer, 0); + } +} diff --git a/src/com/android/gallery3d/data/FilterSource.java b/src/com/android/gallery3d/data/FilterSource.java new file mode 100644 index 000000000..d1a04c995 --- /dev/null +++ b/src/com/android/gallery3d/data/FilterSource.java @@ -0,0 +1,52 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; + +class FilterSource extends MediaSource { + private static final String TAG = "FilterSource"; + private static final int FILTER_BY_MEDIATYPE = 0; + + private GalleryApp mApplication; + private PathMatcher mMatcher; + + public FilterSource(GalleryApp application) { + super("filter"); + mApplication = application; + mMatcher = new PathMatcher(); + mMatcher.add("/filter/mediatype/*/*", FILTER_BY_MEDIATYPE); + } + + // The name we accept is: + // /filter/mediatype/k/{set} + // where k is the media type we want. + @Override + public MediaObject createMediaObject(Path path) { + int matchType = mMatcher.match(path); + int mediaType = mMatcher.getIntVar(0); + String setsName = mMatcher.getVar(1); + DataManager dataManager = mApplication.getDataManager(); + MediaSet[] sets = dataManager.getMediaSetsFromString(setsName); + switch (matchType) { + case FILTER_BY_MEDIATYPE: + return new FilterSet(path, dataManager, sets[0], mediaType); + default: + throw new RuntimeException("bad path: " + path); + } + } +} diff --git a/src/com/android/gallery3d/data/ImageCacheRequest.java b/src/com/android/gallery3d/data/ImageCacheRequest.java new file mode 100644 index 000000000..104ff4839 --- /dev/null +++ b/src/com/android/gallery3d/data/ImageCacheRequest.java @@ -0,0 +1,89 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.data.ImageCacheService.ImageData; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +abstract class ImageCacheRequest implements Job<Bitmap> { + private static final String TAG = "ImageCacheRequest"; + + protected GalleryApp mApplication; + private Path mPath; + private int mType; + private int mTargetSize; + + public ImageCacheRequest(GalleryApp application, + Path path, int type, int targetSize) { + mApplication = application; + mPath = path; + mType = type; + mTargetSize = targetSize; + } + + public Bitmap run(JobContext jc) { + String debugTag = mPath + "," + + ((mType == MediaItem.TYPE_THUMBNAIL) ? "THUMB" : + (mType == MediaItem.TYPE_MICROTHUMBNAIL) ? "MICROTHUMB" : "?"); + ImageCacheService cacheService = mApplication.getImageCacheService(); + + ImageData data = cacheService.getImageData(mPath, mType); + if (jc.isCancelled()) return null; + + if (data != null) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + Bitmap bitmap = DecodeUtils.requestDecode(jc, data.mData, + data.mOffset, data.mData.length - data.mOffset, options); + if (bitmap == null && !jc.isCancelled()) { + Log.w(TAG, "decode cached failed " + debugTag); + } + return bitmap; + } else { + Bitmap bitmap = onDecodeOriginal(jc, mType); + if (jc.isCancelled()) return null; + + if (bitmap == null) { + Log.w(TAG, "decode orig failed " + debugTag); + return null; + } + + if (mType == MediaItem.TYPE_MICROTHUMBNAIL) { + bitmap = BitmapUtils.resizeDownAndCropCenter(bitmap, + mTargetSize, true); + } else { + bitmap = BitmapUtils.resizeDownBySideLength(bitmap, + mTargetSize, true); + } + if (jc.isCancelled()) return null; + + byte[] array = BitmapUtils.compressBitmap(bitmap); + if (jc.isCancelled()) return null; + + cacheService.putImageData(mPath, mType, array); + return bitmap; + } + } + + public abstract Bitmap onDecodeOriginal(JobContext jc, int targetSize); +} diff --git a/src/com/android/gallery3d/data/ImageCacheService.java b/src/com/android/gallery3d/data/ImageCacheService.java new file mode 100644 index 000000000..3adce1332 --- /dev/null +++ b/src/com/android/gallery3d/data/ImageCacheService.java @@ -0,0 +1,105 @@ +/* + * 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.data; + +import com.android.gallery3d.common.BlobCache; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.CacheManager; +import com.android.gallery3d.util.GalleryUtils; + +import android.content.Context; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public class ImageCacheService { + @SuppressWarnings("unused") + private static final String TAG = "ImageCacheService"; + + private static final String IMAGE_CACHE_FILE = "imgcache"; + private static final int IMAGE_CACHE_MAX_ENTRIES = 5000; + private static final int IMAGE_CACHE_MAX_BYTES = 200 * 1024 * 1024; + private static final int IMAGE_CACHE_VERSION = 3; + + private BlobCache mCache; + + public ImageCacheService(Context context) { + mCache = CacheManager.getCache(context, IMAGE_CACHE_FILE, + IMAGE_CACHE_MAX_ENTRIES, IMAGE_CACHE_MAX_BYTES, + IMAGE_CACHE_VERSION); + } + + public static class ImageData { + public ImageData(byte[] data, int offset) { + mData = data; + mOffset = offset; + } + public byte[] mData; + public int mOffset; + } + + public ImageData getImageData(Path path, int type) { + byte[] key = makeKey(path, type); + long cacheKey = Utils.crc64Long(key); + try { + byte[] value = null; + synchronized (mCache) { + value = mCache.lookup(cacheKey); + } + if (value == null) return null; + if (isSameKey(key, value)) { + int offset = key.length; + return new ImageData(value, offset); + } + } catch (IOException ex) { + // ignore. + } + return null; + } + + public void putImageData(Path path, int type, byte[] value) { + byte[] key = makeKey(path, type); + long cacheKey = Utils.crc64Long(key); + ByteBuffer buffer = ByteBuffer.allocate(key.length + value.length); + buffer.put(key); + buffer.put(value); + synchronized (mCache) { + try { + mCache.insert(cacheKey, buffer.array()); + } catch (IOException ex) { + // ignore. + } + } + } + + private static byte[] makeKey(Path path, int type) { + return GalleryUtils.getBytes(path.toString() + "+" + type); + } + + private static boolean isSameKey(byte[] key, byte[] buffer) { + int n = key.length; + if (buffer.length < n) { + return false; + } + for (int i = 0; i < n; ++i) { + if (key[i] != buffer[i]) { + return false; + } + } + return true; + } +} diff --git a/src/com/android/gallery3d/data/LocalAlbum.java b/src/com/android/gallery3d/data/LocalAlbum.java new file mode 100644 index 000000000..5bd4398b4 --- /dev/null +++ b/src/com/android/gallery3d/data/LocalAlbum.java @@ -0,0 +1,252 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.GalleryUtils; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Images.ImageColumns; +import android.provider.MediaStore.Video; +import android.provider.MediaStore.Video.VideoColumns; + +import java.util.ArrayList; + +// LocalAlbumSet lists all media items in one bucket on local storage. +// The media items need to be all images or all videos, but not both. +public class LocalAlbum extends MediaSet { + private static final String TAG = "LocalAlbum"; + private static final String[] COUNT_PROJECTION = { "count(*)" }; + + private static final int INVALID_COUNT = -1; + private final String mWhereClause; + private final String mOrderClause; + private final Uri mBaseUri; + private final String[] mProjection; + + private final GalleryApp mApplication; + private final ContentResolver mResolver; + private final int mBucketId; + private final String mBucketName; + private final boolean mIsImage; + private final ChangeNotifier mNotifier; + private final Path mItemPath; + private int mCachedCount = INVALID_COUNT; + + public LocalAlbum(Path path, GalleryApp application, int bucketId, + boolean isImage, String name) { + super(path, nextVersionNumber()); + mApplication = application; + mResolver = application.getContentResolver(); + mBucketId = bucketId; + mBucketName = name; + mIsImage = isImage; + + if (isImage) { + mWhereClause = ImageColumns.BUCKET_ID + " = ?"; + mOrderClause = ImageColumns.DATE_TAKEN + " DESC, " + + ImageColumns._ID + " DESC"; + mBaseUri = Images.Media.EXTERNAL_CONTENT_URI; + mProjection = LocalImage.PROJECTION; + mItemPath = LocalImage.ITEM_PATH; + } else { + mWhereClause = VideoColumns.BUCKET_ID + " = ?"; + mOrderClause = VideoColumns.DATE_TAKEN + " DESC, " + + VideoColumns._ID + " DESC"; + mBaseUri = Video.Media.EXTERNAL_CONTENT_URI; + mProjection = LocalVideo.PROJECTION; + mItemPath = LocalVideo.ITEM_PATH; + } + + mNotifier = new ChangeNotifier(this, mBaseUri, application); + } + + public LocalAlbum(Path path, GalleryApp application, int bucketId, + boolean isImage) { + this(path, application, bucketId, isImage, + LocalAlbumSet.getBucketName(application.getContentResolver(), + bucketId)); + } + + @Override + public ArrayList<MediaItem> getMediaItem(int start, int count) { + DataManager dataManager = mApplication.getDataManager(); + Uri uri = mBaseUri.buildUpon() + .appendQueryParameter("limit", start + "," + count).build(); + ArrayList<MediaItem> list = new ArrayList<MediaItem>(); + GalleryUtils.assertNotInRenderThread(); + Cursor cursor = mResolver.query( + uri, mProjection, mWhereClause, + new String[]{String.valueOf(mBucketId)}, + mOrderClause); + if (cursor == null) { + Log.w(TAG, "query fail: " + uri); + return list; + } + + try { + while (cursor.moveToNext()) { + int id = cursor.getInt(0); // _id must be in the first column + Path childPath = mItemPath.getChild(id); + MediaItem item = loadOrUpdateItem(childPath, cursor, + dataManager, mApplication, mIsImage); + list.add(item); + } + } finally { + cursor.close(); + } + return list; + } + + private static MediaItem loadOrUpdateItem(Path path, Cursor cursor, + DataManager dataManager, GalleryApp app, boolean isImage) { + LocalMediaItem item = (LocalMediaItem) dataManager.peekMediaObject(path); + if (item == null) { + if (isImage) { + item = new LocalImage(path, app, cursor); + } else { + item = new LocalVideo(path, app, cursor); + } + } else { + item.updateContent(cursor); + } + return item; + } + + // The pids array are sorted by the (path) id. + public static MediaItem[] getMediaItemById( + GalleryApp application, boolean isImage, ArrayList<Integer> ids) { + // get the lower and upper bound of (path) id + MediaItem[] result = new MediaItem[ids.size()]; + if (ids.isEmpty()) return result; + int idLow = ids.get(0); + int idHigh = ids.get(ids.size() - 1); + + // prepare the query parameters + Uri baseUri; + String[] projection; + Path itemPath; + if (isImage) { + baseUri = Images.Media.EXTERNAL_CONTENT_URI; + projection = LocalImage.PROJECTION; + itemPath = LocalImage.ITEM_PATH; + } else { + baseUri = Video.Media.EXTERNAL_CONTENT_URI; + projection = LocalVideo.PROJECTION; + itemPath = LocalVideo.ITEM_PATH; + } + + ContentResolver resolver = application.getContentResolver(); + DataManager dataManager = application.getDataManager(); + Cursor cursor = resolver.query(baseUri, projection, "_id BETWEEN ? AND ?", + new String[]{String.valueOf(idLow), String.valueOf(idHigh)}, + "_id"); + if (cursor == null) { + Log.w(TAG, "query fail" + baseUri); + return result; + } + try { + int n = ids.size(); + int i = 0; + + while (i < n && cursor.moveToNext()) { + int id = cursor.getInt(0); // _id must be in the first column + + // Match id with the one on the ids list. + if (ids.get(i) > id) { + continue; + } + + while (ids.get(i) < id) { + if (++i >= n) { + return result; + } + } + + Path childPath = itemPath.getChild(id); + MediaItem item = loadOrUpdateItem(childPath, cursor, dataManager, + application, isImage); + result[i] = item; + ++i; + } + return result; + } finally { + cursor.close(); + } + } + + public static Cursor getItemCursor(ContentResolver resolver, Uri uri, + String[] projection, int id) { + return resolver.query(uri, projection, "_id=?", + new String[]{String.valueOf(id)}, null); + } + + @Override + public int getMediaItemCount() { + if (mCachedCount == INVALID_COUNT) { + Cursor cursor = mResolver.query( + mBaseUri, COUNT_PROJECTION, mWhereClause, + new String[]{String.valueOf(mBucketId)}, null); + if (cursor == null) { + Log.w(TAG, "query fail"); + return 0; + } + try { + Utils.assertTrue(cursor.moveToNext()); + mCachedCount = cursor.getInt(0); + } finally { + cursor.close(); + } + } + return mCachedCount; + } + + @Override + public String getName() { + return mBucketName; + } + + @Override + public long reload() { + if (mNotifier.isDirty()) { + mDataVersion = nextVersionNumber(); + mCachedCount = INVALID_COUNT; + } + return mDataVersion; + } + + @Override + public int getSupportedOperations() { + return SUPPORT_DELETE | SUPPORT_SHARE | SUPPORT_INFO; + } + + @Override + public void delete() { + GalleryUtils.assertNotInRenderThread(); + mResolver.delete(mBaseUri, mWhereClause, + new String[]{String.valueOf(mBucketId)}); + } + + @Override + public boolean isLeafAlbum() { + return true; + } +} diff --git a/src/com/android/gallery3d/data/LocalAlbumSet.java b/src/com/android/gallery3d/data/LocalAlbumSet.java new file mode 100644 index 000000000..60bef9a33 --- /dev/null +++ b/src/com/android/gallery3d/data/LocalAlbumSet.java @@ -0,0 +1,263 @@ +/* + * 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.data; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.MediaSetUtils; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; +import android.provider.MediaStore.Files; +import android.provider.MediaStore.Files.FileColumns; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Images.ImageColumns; +import android.provider.MediaStore.Video; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashSet; + +// LocalAlbumSet lists all image or video albums in the local storage. +// The path should be "/local/image", "local/video" or "/local/all" +public class LocalAlbumSet extends MediaSet { + public static final Path PATH_ALL = Path.fromString("/local/all"); + public static final Path PATH_IMAGE = Path.fromString("/local/image"); + public static final Path PATH_VIDEO = Path.fromString("/local/video"); + + private static final String TAG = "LocalAlbumSet"; + private static final String EXTERNAL_MEDIA = "external"; + + // The indices should match the following projections. + private static final int INDEX_BUCKET_ID = 0; + private static final int INDEX_MEDIA_TYPE = 1; + private static final int INDEX_BUCKET_NAME = 2; + + private static final Uri mBaseUri = Files.getContentUri(EXTERNAL_MEDIA); + private static final Uri mWatchUriImage = Images.Media.EXTERNAL_CONTENT_URI; + private static final Uri mWatchUriVideo = Video.Media.EXTERNAL_CONTENT_URI; + + // The order is import it must match to the index in MediaStore. + private static final String[] PROJECTION_BUCKET = { + ImageColumns.BUCKET_ID, + FileColumns.MEDIA_TYPE, + ImageColumns.BUCKET_DISPLAY_NAME }; + + private final GalleryApp mApplication; + private final int mType; + private ArrayList<MediaSet> mAlbums = new ArrayList<MediaSet>(); + private final ChangeNotifier mNotifierImage; + private final ChangeNotifier mNotifierVideo; + private final String mName; + + public LocalAlbumSet(Path path, GalleryApp application) { + super(path, nextVersionNumber()); + mApplication = application; + mType = getTypeFromPath(path); + mNotifierImage = new ChangeNotifier(this, mWatchUriImage, application); + mNotifierVideo = new ChangeNotifier(this, mWatchUriVideo, application); + mName = application.getResources().getString( + R.string.set_label_local_albums); + } + + private static int getTypeFromPath(Path path) { + String name[] = path.split(); + if (name.length < 2) { + throw new IllegalArgumentException(path.toString()); + } + if ("all".equals(name[1])) return MEDIA_TYPE_ALL; + if ("image".equals(name[1])) return MEDIA_TYPE_IMAGE; + if ("video".equals(name[1])) return MEDIA_TYPE_VIDEO; + throw new IllegalArgumentException(path.toString()); + } + + @Override + public MediaSet getSubMediaSet(int index) { + return mAlbums.get(index); + } + + @Override + public int getSubMediaSetCount() { + return mAlbums.size(); + } + + @Override + public String getName() { + return mName; + } + + private BucketEntry[] loadBucketEntries(Cursor cursor) { + HashSet<BucketEntry> buffer = new HashSet<BucketEntry>(); + int typeBits = 0; + if ((mType & MEDIA_TYPE_IMAGE) != 0) { + typeBits |= (1 << FileColumns.MEDIA_TYPE_IMAGE); + } + if ((mType & MEDIA_TYPE_VIDEO) != 0) { + typeBits |= (1 << FileColumns.MEDIA_TYPE_VIDEO); + } + try { + while (cursor.moveToNext()) { + if ((typeBits & (1 << cursor.getInt(INDEX_MEDIA_TYPE))) != 0) { + buffer.add(new BucketEntry( + cursor.getInt(INDEX_BUCKET_ID), + cursor.getString(INDEX_BUCKET_NAME))); + } + } + } finally { + cursor.close(); + } + return buffer.toArray(new BucketEntry[buffer.size()]); + } + + + private static int findBucket(BucketEntry entries[], int bucketId) { + for (int i = 0, n = entries.length; i < n ; ++i) { + if (entries[i].bucketId == bucketId) return i; + } + return -1; + } + + @SuppressWarnings("unchecked") + protected ArrayList<MediaSet> loadSubMediaSets() { + // Note: it will be faster if we only select media_type and bucket_id. + // need to test the performance if that is worth + + Uri uri = mBaseUri.buildUpon(). + appendQueryParameter("distinct", "true").build(); + GalleryUtils.assertNotInRenderThread(); + Cursor cursor = mApplication.getContentResolver().query( + uri, PROJECTION_BUCKET, null, null, null); + if (cursor == null) { + Log.w(TAG, "cannot open local database: " + uri); + return new ArrayList<MediaSet>(); + } + BucketEntry[] entries = loadBucketEntries(cursor); + int offset = 0; + + int index = findBucket(entries, MediaSetUtils.CAMERA_BUCKET_ID); + if (index != -1) { + Utils.swap(entries, index, offset++); + } + index = findBucket(entries, MediaSetUtils.DOWNLOAD_BUCKET_ID); + if (index != -1) { + Utils.swap(entries, index, offset++); + } + + Arrays.sort(entries, offset, entries.length, new Comparator<BucketEntry>() { + @Override + public int compare(BucketEntry a, BucketEntry b) { + int result = a.bucketName.compareTo(b.bucketName); + return result != 0 + ? result + : Utils.compare(a.bucketId, b.bucketId); + } + }); + ArrayList<MediaSet> albums = new ArrayList<MediaSet>(); + DataManager dataManager = mApplication.getDataManager(); + for (BucketEntry entry : entries) { + albums.add(getLocalAlbum(dataManager, + mType, mPath, entry.bucketId, entry.bucketName)); + } + for (int i = 0, n = albums.size(); i < n; ++i) { + albums.get(i).reload(); + } + return albums; + } + + private MediaSet getLocalAlbum( + DataManager manager, int type, Path parent, int id, String name) { + Path path = parent.getChild(id); + MediaObject object = manager.peekMediaObject(path); + if (object != null) return (MediaSet) object; + switch (type) { + case MEDIA_TYPE_IMAGE: + return new LocalAlbum(path, mApplication, id, true, name); + case MEDIA_TYPE_VIDEO: + return new LocalAlbum(path, mApplication, id, false, name); + case MEDIA_TYPE_ALL: + Comparator<MediaItem> comp = DataManager.sDateTakenComparator; + return new LocalMergeAlbum(path, comp, new MediaSet[] { + getLocalAlbum(manager, MEDIA_TYPE_IMAGE, PATH_IMAGE, id, name), + getLocalAlbum(manager, MEDIA_TYPE_VIDEO, PATH_VIDEO, id, name)}); + } + throw new IllegalArgumentException(String.valueOf(type)); + } + + public static String getBucketName(ContentResolver resolver, int bucketId) { + Uri uri = mBaseUri.buildUpon() + .appendQueryParameter("limit", "1") + .build(); + + Cursor cursor = resolver.query( + uri, PROJECTION_BUCKET, "bucket_id = ?", + new String[]{String.valueOf(bucketId)}, null); + + if (cursor == null) { + Log.w(TAG, "query fail: " + uri); + return ""; + } + try { + return cursor.moveToNext() + ? cursor.getString(INDEX_BUCKET_NAME) + : ""; + } finally { + cursor.close(); + } + } + + @Override + public long reload() { + // "|" is used instead of "||" because we want to clear both flags. + if (mNotifierImage.isDirty() | mNotifierVideo.isDirty()) { + mDataVersion = nextVersionNumber(); + mAlbums = loadSubMediaSets(); + } + return mDataVersion; + } + + // For debug only. Fake there is a ContentObserver.onChange() event. + void fakeChange() { + mNotifierImage.fakeChange(); + mNotifierVideo.fakeChange(); + } + + private static class BucketEntry { + public String bucketName; + public int bucketId; + + public BucketEntry(int id, String name) { + bucketId = id; + bucketName = Utils.ensureNotNull(name); + } + + @Override + public int hashCode() { + return bucketId; + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof BucketEntry)) return false; + BucketEntry entry = (BucketEntry) object; + return bucketId == entry.bucketId; + } + } +} diff --git a/src/com/android/gallery3d/data/LocalImage.java b/src/com/android/gallery3d/data/LocalImage.java new file mode 100644 index 000000000..f3dedf037 --- /dev/null +++ b/src/com/android/gallery3d/data/LocalImage.java @@ -0,0 +1,289 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.util.UpdateHelper; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.media.ExifInterface; +import android.net.Uri; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Images.ImageColumns; + +import java.io.File; +import java.io.IOException; + +// LocalImage represents an image in the local storage. +public class LocalImage extends LocalMediaItem { + private static final int THUMBNAIL_TARGET_SIZE = 640; + private static final int MICROTHUMBNAIL_TARGET_SIZE = 200; + + private static final String TAG = "LocalImage"; + + static final Path ITEM_PATH = Path.fromString("/local/image/item"); + + // Must preserve order between these indices and the order of the terms in + // the following PROJECTION array. + private static final int INDEX_ID = 0; + private static final int INDEX_CAPTION = 1; + private static final int INDEX_MIME_TYPE = 2; + private static final int INDEX_LATITUDE = 3; + private static final int INDEX_LONGITUDE = 4; + private static final int INDEX_DATE_TAKEN = 5; + private static final int INDEX_DATE_ADDED = 6; + private static final int INDEX_DATE_MODIFIED = 7; + private static final int INDEX_DATA = 8; + private static final int INDEX_ORIENTATION = 9; + private static final int INDEX_BUCKET_ID = 10; + private static final int INDEX_SIZE_ID = 11; + + static final String[] PROJECTION = { + ImageColumns._ID, // 0 + ImageColumns.TITLE, // 1 + ImageColumns.MIME_TYPE, // 2 + ImageColumns.LATITUDE, // 3 + ImageColumns.LONGITUDE, // 4 + ImageColumns.DATE_TAKEN, // 5 + ImageColumns.DATE_ADDED, // 6 + ImageColumns.DATE_MODIFIED, // 7 + ImageColumns.DATA, // 8 + ImageColumns.ORIENTATION, // 9 + ImageColumns.BUCKET_ID, // 10 + ImageColumns.SIZE // 11 + }; + + private final GalleryApp mApplication; + + public int rotation; + + public LocalImage(Path path, GalleryApp application, Cursor cursor) { + super(path, nextVersionNumber()); + mApplication = application; + loadFromCursor(cursor); + } + + public LocalImage(Path path, GalleryApp application, int id) { + super(path, nextVersionNumber()); + mApplication = application; + ContentResolver resolver = mApplication.getContentResolver(); + Uri uri = Images.Media.EXTERNAL_CONTENT_URI; + Cursor cursor = LocalAlbum.getItemCursor(resolver, uri, PROJECTION, id); + if (cursor == null) { + throw new RuntimeException("cannot get cursor for: " + path); + } + try { + if (cursor.moveToNext()) { + loadFromCursor(cursor); + } else { + throw new RuntimeException("cannot find data for: " + path); + } + } finally { + cursor.close(); + } + } + + private void loadFromCursor(Cursor cursor) { + id = cursor.getInt(INDEX_ID); + caption = cursor.getString(INDEX_CAPTION); + mimeType = cursor.getString(INDEX_MIME_TYPE); + latitude = cursor.getDouble(INDEX_LATITUDE); + longitude = cursor.getDouble(INDEX_LONGITUDE); + dateTakenInMs = cursor.getLong(INDEX_DATE_TAKEN); + filePath = cursor.getString(INDEX_DATA); + rotation = cursor.getInt(INDEX_ORIENTATION); + bucketId = cursor.getInt(INDEX_BUCKET_ID); + fileSize = cursor.getLong(INDEX_SIZE_ID); + } + + @Override + protected boolean updateFromCursor(Cursor cursor) { + UpdateHelper uh = new UpdateHelper(); + id = uh.update(id, cursor.getInt(INDEX_ID)); + caption = uh.update(caption, cursor.getString(INDEX_CAPTION)); + mimeType = uh.update(mimeType, cursor.getString(INDEX_MIME_TYPE)); + latitude = uh.update(latitude, cursor.getDouble(INDEX_LATITUDE)); + longitude = uh.update(longitude, cursor.getDouble(INDEX_LONGITUDE)); + dateTakenInMs = uh.update( + dateTakenInMs, cursor.getLong(INDEX_DATE_TAKEN)); + dateAddedInSec = uh.update( + dateAddedInSec, cursor.getLong(INDEX_DATE_ADDED)); + dateModifiedInSec = uh.update( + dateModifiedInSec, cursor.getLong(INDEX_DATE_MODIFIED)); + filePath = uh.update(filePath, cursor.getString(INDEX_DATA)); + rotation = uh.update(rotation, cursor.getInt(INDEX_ORIENTATION)); + bucketId = uh.update(bucketId, cursor.getInt(INDEX_BUCKET_ID)); + fileSize = uh.update(fileSize, cursor.getLong(INDEX_SIZE_ID)); + return uh.isUpdated(); + } + + @Override + public Job<Bitmap> requestImage(int type) { + return new LocalImageRequest(mApplication, mPath, type, filePath); + } + + public static class LocalImageRequest extends ImageCacheRequest { + private String mLocalFilePath; + + LocalImageRequest(GalleryApp application, Path path, int type, + String localFilePath) { + super(application, path, type, getTargetSize(type)); + mLocalFilePath = localFilePath; + } + + @Override + public Bitmap onDecodeOriginal(JobContext jc, int type) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + return DecodeUtils.requestDecode( + jc, mLocalFilePath, options, getTargetSize(type)); + } + } + + static int getTargetSize(int type) { + switch (type) { + case TYPE_THUMBNAIL: + return THUMBNAIL_TARGET_SIZE; + case TYPE_MICROTHUMBNAIL: + return MICROTHUMBNAIL_TARGET_SIZE; + default: + throw new RuntimeException( + "should only request thumb/microthumb from cache"); + } + } + + @Override + public Job<BitmapRegionDecoder> requestLargeImage() { + return new LocalLargeImageRequest(filePath); + } + + public static class LocalLargeImageRequest + implements Job<BitmapRegionDecoder> { + String mLocalFilePath; + + public LocalLargeImageRequest(String localFilePath) { + mLocalFilePath = localFilePath; + } + + public BitmapRegionDecoder run(JobContext jc) { + return DecodeUtils.requestCreateBitmapRegionDecoder( + jc, mLocalFilePath, false); + } + } + + @Override + public int getSupportedOperations() { + int operation = SUPPORT_DELETE | SUPPORT_SHARE | SUPPORT_CROP + | SUPPORT_SETAS | SUPPORT_EDIT | SUPPORT_INFO; + if (BitmapUtils.isSupportedByRegionDecoder(mimeType)) { + operation |= SUPPORT_FULL_IMAGE; + } + + if (BitmapUtils.isRotationSupported(mimeType)) { + operation |= SUPPORT_ROTATE; + } + + if (GalleryUtils.isValidLocation(latitude, longitude)) { + operation |= SUPPORT_SHOW_ON_MAP; + } + return operation; + } + + @Override + public void delete() { + GalleryUtils.assertNotInRenderThread(); + Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI; + mApplication.getContentResolver().delete(baseUri, "_id=?", + new String[]{String.valueOf(id)}); + } + + private static String getExifOrientation(int orientation) { + switch (orientation) { + case 0: + return String.valueOf(ExifInterface.ORIENTATION_NORMAL); + case 90: + return String.valueOf(ExifInterface.ORIENTATION_ROTATE_90); + case 180: + return String.valueOf(ExifInterface.ORIENTATION_ROTATE_180); + case 270: + return String.valueOf(ExifInterface.ORIENTATION_ROTATE_270); + default: + throw new AssertionError("invalid: " + orientation); + } + } + + @Override + public void rotate(int degrees) { + GalleryUtils.assertNotInRenderThread(); + Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI; + ContentValues values = new ContentValues(); + int rotation = (this.rotation + degrees) % 360; + if (rotation < 0) rotation += 360; + + if (mimeType.equalsIgnoreCase("image/jpeg")) { + try { + ExifInterface exif = new ExifInterface(filePath); + exif.setAttribute(ExifInterface.TAG_ORIENTATION, + getExifOrientation(rotation)); + exif.saveAttributes(); + } catch (IOException e) { + Log.w(TAG, "cannot set exif data: " + filePath); + } + + // We need to update the filesize as well + fileSize = new File(filePath).length(); + values.put(Images.Media.SIZE, fileSize); + } + + values.put(Images.Media.ORIENTATION, rotation); + mApplication.getContentResolver().update(baseUri, values, "_id=?", + new String[]{String.valueOf(id)}); + } + + @Override + public Uri getContentUri() { + Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI; + return baseUri.buildUpon().appendPath(String.valueOf(id)).build(); + } + + @Override + public int getMediaType() { + return MEDIA_TYPE_IMAGE; + } + + @Override + public MediaDetails getDetails() { + MediaDetails details = super.getDetails(); + details.addDetail(MediaDetails.INDEX_ORIENTATION, Integer.valueOf(rotation)); + MediaDetails.extractExifInfo(details, filePath); + return details; + } + + @Override + public int getRotation() { + return rotation; + } +} diff --git a/src/com/android/gallery3d/data/LocalMediaItem.java b/src/com/android/gallery3d/data/LocalMediaItem.java new file mode 100644 index 000000000..a76fedf32 --- /dev/null +++ b/src/com/android/gallery3d/data/LocalMediaItem.java @@ -0,0 +1,103 @@ +/* + * 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.data; + +import com.android.gallery3d.util.GalleryUtils; + +import android.database.Cursor; + +import java.text.DateFormat; +import java.util.Date; + +// +// LocalMediaItem is an abstract class captures those common fields +// in LocalImage and LocalVideo. +// +public abstract class LocalMediaItem extends MediaItem { + + @SuppressWarnings("unused") + private static final String TAG = "LocalMediaItem"; + + // database fields + public int id; + public String caption; + public String mimeType; + public long fileSize; + public double latitude = INVALID_LATLNG; + public double longitude = INVALID_LATLNG; + public long dateTakenInMs; + public long dateAddedInSec; + public long dateModifiedInSec; + public String filePath; + public int bucketId; + + public LocalMediaItem(Path path, long version) { + super(path, version); + } + + @Override + public long getDateInMs() { + return dateTakenInMs; + } + + @Override + public String getName() { + return caption; + } + + @Override + public void getLatLong(double[] latLong) { + latLong[0] = latitude; + latLong[1] = longitude; + } + + abstract protected boolean updateFromCursor(Cursor cursor); + + public int getBucketId() { + return bucketId; + } + + protected void updateContent(Cursor cursor) { + if (updateFromCursor(cursor)) { + mDataVersion = nextVersionNumber(); + } + } + + @Override + public MediaDetails getDetails() { + MediaDetails details = super.getDetails(); + details.addDetail(MediaDetails.INDEX_PATH, filePath); + details.addDetail(MediaDetails.INDEX_TITLE, caption); + DateFormat formater = DateFormat.getDateTimeInstance(); + details.addDetail(MediaDetails.INDEX_DATETIME, formater.format(new Date(dateTakenInMs))); + + if (GalleryUtils.isValidLocation(latitude, longitude)) { + details.addDetail(MediaDetails.INDEX_LOCATION, new double[] {latitude, longitude}); + } + if (fileSize > 0) details.addDetail(MediaDetails.INDEX_SIZE, fileSize); + return details; + } + + @Override + public String getMimeType() { + return mimeType; + } + + public long getSize() { + return fileSize; + } +} diff --git a/src/com/android/gallery3d/data/LocalMergeAlbum.java b/src/com/android/gallery3d/data/LocalMergeAlbum.java new file mode 100644 index 000000000..bb796d53a --- /dev/null +++ b/src/com/android/gallery3d/data/LocalMergeAlbum.java @@ -0,0 +1,226 @@ +/* + * 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.data; + +import java.lang.ref.SoftReference; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.SortedMap; +import java.util.TreeMap; + +// MergeAlbum merges items from two or more MediaSets. It uses a Comparator to +// determine the order of items. The items are assumed to be sorted in the input +// media sets (with the same order that the Comparator uses). +// +// This only handles MediaItems, not SubMediaSets. +public class LocalMergeAlbum extends MediaSet implements ContentListener { + @SuppressWarnings("unused") + private static final String TAG = "LocalMergeAlbum"; + private static final int PAGE_SIZE = 64; + + private final Comparator<MediaItem> mComparator; + private final MediaSet[] mSources; + + private String mName; + private FetchCache[] mFetcher; + private int mSupportedOperation; + + // mIndex maps global position to the position of each underlying media sets. + private TreeMap<Integer, int[]> mIndex = new TreeMap<Integer, int[]>(); + + public LocalMergeAlbum( + Path path, Comparator<MediaItem> comparator, MediaSet[] sources) { + super(path, INVALID_DATA_VERSION); + mComparator = comparator; + mSources = sources; + mName = sources.length == 0 ? "" : sources[0].getName(); + for (MediaSet set : mSources) { + set.addContentListener(this); + } + } + + private void updateData() { + ArrayList<MediaSet> matches = new ArrayList<MediaSet>(); + int supported = mSources.length == 0 ? 0 : MediaItem.SUPPORT_ALL; + mFetcher = new FetchCache[mSources.length]; + for (int i = 0, n = mSources.length; i < n; ++i) { + mFetcher[i] = new FetchCache(mSources[i]); + supported &= mSources[i].getSupportedOperations(); + } + mSupportedOperation = supported; + mIndex.clear(); + mIndex.put(0, new int[mSources.length]); + mName = mSources.length == 0 ? "" : mSources[0].getName(); + } + + private void invalidateCache() { + for (int i = 0, n = mSources.length; i < n; i++) { + mFetcher[i].invalidate(); + } + mIndex.clear(); + mIndex.put(0, new int[mSources.length]); + } + + @Override + public String getName() { + return mName; + } + + @Override + public int getMediaItemCount() { + return getTotalMediaItemCount(); + } + + @Override + public ArrayList<MediaItem> getMediaItem(int start, int count) { + + // First find the nearest mark position <= start. + SortedMap<Integer, int[]> head = mIndex.headMap(start + 1); + int markPos = head.lastKey(); + int[] subPos = head.get(markPos).clone(); + MediaItem[] slot = new MediaItem[mSources.length]; + + int size = mSources.length; + + // fill all slots + for (int i = 0; i < size; i++) { + slot[i] = mFetcher[i].getItem(subPos[i]); + } + + ArrayList<MediaItem> result = new ArrayList<MediaItem>(); + + for (int i = markPos; i < start + count; i++) { + int k = -1; // k points to the best slot up to now. + for (int j = 0; j < size; j++) { + if (slot[j] != null) { + if (k == -1 || mComparator.compare(slot[j], slot[k]) < 0) { + k = j; + } + } + } + + // If we don't have anything, all streams are exhausted. + if (k == -1) break; + + // Pick the best slot and refill it. + subPos[k]++; + if (i >= start) { + result.add(slot[k]); + } + slot[k] = mFetcher[k].getItem(subPos[k]); + + // Periodically leave a mark in the index, so we can come back later. + if ((i + 1) % PAGE_SIZE == 0) { + mIndex.put(i + 1, subPos.clone()); + } + } + + return result; + } + + @Override + public int getTotalMediaItemCount() { + int count = 0; + for (MediaSet set : mSources) { + count += set.getTotalMediaItemCount(); + } + return count; + } + + @Override + public long reload() { + boolean changed = false; + for (int i = 0, n = mSources.length; i < n; ++i) { + if (mSources[i].reload() > mDataVersion) changed = true; + } + if (changed) { + mDataVersion = nextVersionNumber(); + updateData(); + invalidateCache(); + } + return mDataVersion; + } + + @Override + public void onContentDirty() { + notifyContentChanged(); + } + + @Override + public int getSupportedOperations() { + return mSupportedOperation; + } + + @Override + public void delete() { + for (MediaSet set : mSources) { + set.delete(); + } + } + + @Override + public void rotate(int degrees) { + for (MediaSet set : mSources) { + set.rotate(degrees); + } + } + + private static class FetchCache { + private MediaSet mBaseSet; + private SoftReference<ArrayList<MediaItem>> mCacheRef; + private int mStartPos; + + public FetchCache(MediaSet baseSet) { + mBaseSet = baseSet; + } + + public void invalidate() { + mCacheRef = null; + } + + public MediaItem getItem(int index) { + boolean needLoading = false; + ArrayList<MediaItem> cache = null; + if (mCacheRef == null + || index < mStartPos || index >= mStartPos + PAGE_SIZE) { + needLoading = true; + } else { + cache = mCacheRef.get(); + if (cache == null) { + needLoading = true; + } + } + + if (needLoading) { + cache = mBaseSet.getMediaItem(index, PAGE_SIZE); + mCacheRef = new SoftReference<ArrayList<MediaItem>>(cache); + mStartPos = index; + } + + if (index < mStartPos || index >= mStartPos + cache.size()) { + return null; + } + + return cache.get(index - mStartPos); + } + } + + @Override + public boolean isLeafAlbum() { + return true; + } +} diff --git a/src/com/android/gallery3d/data/LocalSource.java b/src/com/android/gallery3d/data/LocalSource.java new file mode 100644 index 000000000..58ac22490 --- /dev/null +++ b/src/com/android/gallery3d/data/LocalSource.java @@ -0,0 +1,272 @@ +/* + * 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.data; + +import com.android.gallery3d.app.Gallery; +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.data.MediaSet.ItemConsumer; + +import android.content.ContentProviderClient; +import android.content.ContentUris; +import android.content.UriMatcher; +import android.net.Uri; +import android.provider.MediaStore; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +class LocalSource extends MediaSource { + + public static final String KEY_BUCKET_ID = "bucketId"; + + private GalleryApp mApplication; + private PathMatcher mMatcher; + private static final int NO_MATCH = -1; + private final UriMatcher mUriMatcher = new UriMatcher(NO_MATCH); + public static final Comparator<PathId> sIdComparator = new IdComparator(); + + private static final int LOCAL_IMAGE_ALBUMSET = 0; + private static final int LOCAL_VIDEO_ALBUMSET = 1; + private static final int LOCAL_IMAGE_ALBUM = 2; + private static final int LOCAL_VIDEO_ALBUM = 3; + private static final int LOCAL_IMAGE_ITEM = 4; + private static final int LOCAL_VIDEO_ITEM = 5; + private static final int LOCAL_ALL_ALBUMSET = 6; + private static final int LOCAL_ALL_ALBUM = 7; + + private static final String TAG = "LocalSource"; + + private ContentProviderClient mClient; + + public LocalSource(GalleryApp context) { + super("local"); + mApplication = context; + mMatcher = new PathMatcher(); + mMatcher.add("/local/image", LOCAL_IMAGE_ALBUMSET); + mMatcher.add("/local/video", LOCAL_VIDEO_ALBUMSET); + mMatcher.add("/local/all", LOCAL_ALL_ALBUMSET); + + mMatcher.add("/local/image/*", LOCAL_IMAGE_ALBUM); + mMatcher.add("/local/video/*", LOCAL_VIDEO_ALBUM); + mMatcher.add("/local/all/*", LOCAL_ALL_ALBUM); + mMatcher.add("/local/image/item/*", LOCAL_IMAGE_ITEM); + mMatcher.add("/local/video/item/*", LOCAL_VIDEO_ITEM); + + mUriMatcher.addURI(MediaStore.AUTHORITY, + "external/images/media/#", LOCAL_IMAGE_ITEM); + mUriMatcher.addURI(MediaStore.AUTHORITY, + "external/video/media/#", LOCAL_VIDEO_ITEM); + mUriMatcher.addURI(MediaStore.AUTHORITY, + "external/images/media", LOCAL_IMAGE_ALBUM); + mUriMatcher.addURI(MediaStore.AUTHORITY, + "external/video/media", LOCAL_VIDEO_ALBUM); + } + + @Override + public MediaObject createMediaObject(Path path) { + GalleryApp app = mApplication; + switch (mMatcher.match(path)) { + case LOCAL_ALL_ALBUMSET: + case LOCAL_IMAGE_ALBUMSET: + case LOCAL_VIDEO_ALBUMSET: + return new LocalAlbumSet(path, mApplication); + case LOCAL_IMAGE_ALBUM: + return new LocalAlbum(path, app, mMatcher.getIntVar(0), true); + case LOCAL_VIDEO_ALBUM: + return new LocalAlbum(path, app, mMatcher.getIntVar(0), false); + case LOCAL_ALL_ALBUM: { + int bucketId = mMatcher.getIntVar(0); + DataManager dataManager = app.getDataManager(); + MediaSet imageSet = (MediaSet) dataManager.getMediaObject( + LocalAlbumSet.PATH_IMAGE.getChild(bucketId)); + MediaSet videoSet = (MediaSet) dataManager.getMediaObject( + LocalAlbumSet.PATH_VIDEO.getChild(bucketId)); + Comparator<MediaItem> comp = DataManager.sDateTakenComparator; + return new LocalMergeAlbum( + path, comp, new MediaSet[] {imageSet, videoSet}); + } + case LOCAL_IMAGE_ITEM: + return new LocalImage(path, mApplication, mMatcher.getIntVar(0)); + case LOCAL_VIDEO_ITEM: + return new LocalVideo(path, mApplication, mMatcher.getIntVar(0)); + default: + throw new RuntimeException("bad path: " + path); + } + } + + private static int getMediaType(String type, int defaultType) { + if (type == null) return defaultType; + try { + int value = Integer.parseInt(type); + if ((value & (MEDIA_TYPE_IMAGE + | MEDIA_TYPE_VIDEO)) != 0) return value; + } catch (NumberFormatException e) { + Log.w(TAG, "invalid type: " + type, e); + } + return defaultType; + } + + // The media type bit passed by the intent + private static final int MEDIA_TYPE_IMAGE = 1; + private static final int MEDIA_TYPE_VIDEO = 4; + + private Path getAlbumPath(Uri uri, int defaultType) { + int mediaType = getMediaType( + uri.getQueryParameter(Gallery.KEY_MEDIA_TYPES), + defaultType); + String bucketId = uri.getQueryParameter(KEY_BUCKET_ID); + int id = 0; + try { + id = Integer.parseInt(bucketId); + } catch (NumberFormatException e) { + Log.w(TAG, "invalid bucket id: " + bucketId, e); + return null; + } + switch (mediaType) { + case MEDIA_TYPE_IMAGE: + return Path.fromString("/local/image").getChild(id); + case MEDIA_TYPE_VIDEO: + return Path.fromString("/local/video").getChild(id); + default: + return Path.fromString("/merge/{/local/image,/local/video}") + .getChild(id); + } + } + + @Override + public Path findPathByUri(Uri uri) { + try { + switch (mUriMatcher.match(uri)) { + case LOCAL_IMAGE_ITEM: { + long id = ContentUris.parseId(uri); + return id >= 0 ? LocalImage.ITEM_PATH.getChild(id) : null; + } + case LOCAL_VIDEO_ITEM: { + long id = ContentUris.parseId(uri); + return id >= 0 ? LocalVideo.ITEM_PATH.getChild(id) : null; + } + case LOCAL_IMAGE_ALBUM: { + return getAlbumPath(uri, MEDIA_TYPE_IMAGE); + } + case LOCAL_VIDEO_ALBUM: { + return getAlbumPath(uri, MEDIA_TYPE_VIDEO); + } + } + } catch (NumberFormatException e) { + Log.w(TAG, "uri: " + uri.toString(), e); + } + return null; + } + + @Override + public Path getDefaultSetOf(Path item) { + MediaObject object = mApplication.getDataManager().getMediaObject(item); + if (object instanceof LocalImage) { + return Path.fromString("/local/image/").getChild( + String.valueOf(((LocalImage) object).getBucketId())); + } else if (object instanceof LocalVideo) { + return Path.fromString("/local/video/").getChild( + String.valueOf(((LocalVideo) object).getBucketId())); + } + return null; + } + + @Override + public void mapMediaItems(ArrayList<PathId> list, ItemConsumer consumer) { + ArrayList<PathId> imageList = new ArrayList<PathId>(); + ArrayList<PathId> videoList = new ArrayList<PathId>(); + int n = list.size(); + for (int i = 0; i < n; i++) { + PathId pid = list.get(i); + // We assume the form is: "/local/{image,video}/item/#" + // We don't use mMatcher for efficiency's reason. + Path parent = pid.path.getParent(); + if (parent == LocalImage.ITEM_PATH) { + imageList.add(pid); + } else if (parent == LocalVideo.ITEM_PATH) { + videoList.add(pid); + } + } + // TODO: use "files" table so we can merge the two cases. + processMapMediaItems(imageList, consumer, true); + processMapMediaItems(videoList, consumer, false); + } + + private void processMapMediaItems(ArrayList<PathId> list, + ItemConsumer consumer, boolean isImage) { + // Sort path by path id + Collections.sort(list, sIdComparator); + int n = list.size(); + for (int i = 0; i < n; ) { + PathId pid = list.get(i); + + // Find a range of items. + ArrayList<Integer> ids = new ArrayList<Integer>(); + int startId = Integer.parseInt(pid.path.getSuffix()); + ids.add(startId); + + int j; + for (j = i + 1; j < n; j++) { + PathId pid2 = list.get(j); + int curId = Integer.parseInt(pid2.path.getSuffix()); + if (curId - startId >= MediaSet.MEDIAITEM_BATCH_FETCH_COUNT) { + break; + } + ids.add(curId); + } + + MediaItem[] items = LocalAlbum.getMediaItemById( + mApplication, isImage, ids); + for(int k = i ; k < j; k++) { + PathId pid2 = list.get(k); + consumer.consume(pid2.id, items[k - i]); + } + + i = j; + } + } + + // This is a comparator which compares the suffix number in two Paths. + private static class IdComparator implements Comparator<PathId> { + public int compare(PathId p1, PathId p2) { + String s1 = p1.path.getSuffix(); + String s2 = p2.path.getSuffix(); + int len1 = s1.length(); + int len2 = s2.length(); + if (len1 < len2) { + return -1; + } else if (len1 > len2) { + return 1; + } else { + return s1.compareTo(s2); + } + } + } + + @Override + public void resume() { + mClient = mApplication.getContentResolver() + .acquireContentProviderClient(MediaStore.AUTHORITY); + } + + @Override + public void pause() { + mClient.release(); + mClient = null; + } +} diff --git a/src/com/android/gallery3d/data/LocalVideo.java b/src/com/android/gallery3d/data/LocalVideo.java new file mode 100644 index 000000000..d1498e856 --- /dev/null +++ b/src/com/android/gallery3d/data/LocalVideo.java @@ -0,0 +1,213 @@ +/* + * 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.data; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.util.UpdateHelper; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.net.Uri; +import android.provider.MediaStore.Video; +import android.provider.MediaStore.Video.VideoColumns; + +import java.io.File; + +// LocalVideo represents a video in the local storage. +public class LocalVideo extends LocalMediaItem { + + static final Path ITEM_PATH = Path.fromString("/local/video/item"); + + // Must preserve order between these indices and the order of the terms in + // the following PROJECTION array. + private static final int INDEX_ID = 0; + private static final int INDEX_CAPTION = 1; + private static final int INDEX_MIME_TYPE = 2; + private static final int INDEX_LATITUDE = 3; + private static final int INDEX_LONGITUDE = 4; + private static final int INDEX_DATE_TAKEN = 5; + private static final int INDEX_DATE_ADDED = 6; + private static final int INDEX_DATE_MODIFIED = 7; + private static final int INDEX_DATA = 8; + private static final int INDEX_DURATION = 9; + private static final int INDEX_BUCKET_ID = 10; + private static final int INDEX_SIZE_ID = 11; + + static final String[] PROJECTION = new String[] { + VideoColumns._ID, + VideoColumns.TITLE, + VideoColumns.MIME_TYPE, + VideoColumns.LATITUDE, + VideoColumns.LONGITUDE, + VideoColumns.DATE_TAKEN, + VideoColumns.DATE_ADDED, + VideoColumns.DATE_MODIFIED, + VideoColumns.DATA, + VideoColumns.DURATION, + VideoColumns.BUCKET_ID, + VideoColumns.SIZE + }; + + private final GalleryApp mApplication; + private static Bitmap sOverlay; + + public int durationInSec; + + public LocalVideo(Path path, GalleryApp application, Cursor cursor) { + super(path, nextVersionNumber()); + mApplication = application; + loadFromCursor(cursor); + } + + public LocalVideo(Path path, GalleryApp context, int id) { + super(path, nextVersionNumber()); + mApplication = context; + ContentResolver resolver = mApplication.getContentResolver(); + Uri uri = Video.Media.EXTERNAL_CONTENT_URI; + Cursor cursor = LocalAlbum.getItemCursor(resolver, uri, PROJECTION, id); + if (cursor == null) { + throw new RuntimeException("cannot get cursor for: " + path); + } + try { + if (cursor.moveToNext()) { + loadFromCursor(cursor); + } else { + throw new RuntimeException("cannot find data for: " + path); + } + } finally { + cursor.close(); + } + } + + private void loadFromCursor(Cursor cursor) { + id = cursor.getInt(INDEX_ID); + caption = cursor.getString(INDEX_CAPTION); + mimeType = cursor.getString(INDEX_MIME_TYPE); + latitude = cursor.getDouble(INDEX_LATITUDE); + longitude = cursor.getDouble(INDEX_LONGITUDE); + dateTakenInMs = cursor.getLong(INDEX_DATE_TAKEN); + filePath = cursor.getString(INDEX_DATA); + durationInSec = cursor.getInt(INDEX_DURATION) / 1000; + bucketId = cursor.getInt(INDEX_BUCKET_ID); + fileSize = cursor.getLong(INDEX_SIZE_ID); + } + + @Override + protected boolean updateFromCursor(Cursor cursor) { + UpdateHelper uh = new UpdateHelper(); + id = uh.update(id, cursor.getInt(INDEX_ID)); + caption = uh.update(caption, cursor.getString(INDEX_CAPTION)); + mimeType = uh.update(mimeType, cursor.getString(INDEX_MIME_TYPE)); + latitude = uh.update(latitude, cursor.getDouble(INDEX_LATITUDE)); + longitude = uh.update(longitude, cursor.getDouble(INDEX_LONGITUDE)); + dateTakenInMs = uh.update( + dateTakenInMs, cursor.getLong(INDEX_DATE_TAKEN)); + dateAddedInSec = uh.update( + dateAddedInSec, cursor.getLong(INDEX_DATE_ADDED)); + dateModifiedInSec = uh.update( + dateModifiedInSec, cursor.getLong(INDEX_DATE_MODIFIED)); + filePath = uh.update(filePath, cursor.getString(INDEX_DATA)); + durationInSec = uh.update( + durationInSec, cursor.getInt(INDEX_DURATION) / 1000); + bucketId = uh.update(bucketId, cursor.getInt(INDEX_BUCKET_ID)); + fileSize = uh.update(fileSize, cursor.getLong(INDEX_SIZE_ID)); + return uh.isUpdated(); + } + + @Override + public Job<Bitmap> requestImage(int type) { + return new LocalVideoRequest(mApplication, getPath(), type, filePath); + } + + public static class LocalVideoRequest extends ImageCacheRequest { + private String mLocalFilePath; + + LocalVideoRequest(GalleryApp application, Path path, int type, + String localFilePath) { + super(application, path, type, LocalImage.getTargetSize(type)); + mLocalFilePath = localFilePath; + } + + @Override + public Bitmap onDecodeOriginal(JobContext jc, int type) { + Bitmap bitmap = BitmapUtils.createVideoThumbnail(mLocalFilePath); + if (bitmap == null || jc.isCancelled()) return null; + return bitmap; + } + } + + @Override + public Job<BitmapRegionDecoder> requestLargeImage() { + throw new UnsupportedOperationException("Cannot regquest a large image" + + " to a local video!"); + } + + @Override + public int getSupportedOperations() { + return SUPPORT_DELETE | SUPPORT_SHARE | SUPPORT_PLAY | SUPPORT_INFO; + } + + @Override + public void delete() { + GalleryUtils.assertNotInRenderThread(); + Uri baseUri = Video.Media.EXTERNAL_CONTENT_URI; + mApplication.getContentResolver().delete(baseUri, "_id=?", + new String[]{String.valueOf(id)}); + } + + @Override + public void rotate(int degrees) { + // TODO + } + + @Override + public Uri getContentUri() { + Uri baseUri = Video.Media.EXTERNAL_CONTENT_URI; + return baseUri.buildUpon().appendPath(String.valueOf(id)).build(); + } + + @Override + public Uri getPlayUri() { + return Uri.fromFile(new File(filePath)); + } + + @Override + public int getMediaType() { + return MEDIA_TYPE_VIDEO; + } + + @Override + public MediaDetails getDetails() { + MediaDetails details = super.getDetails(); + int s = durationInSec; + if (s > 0) { + details.addDetail(MediaDetails.INDEX_DURATION, GalleryUtils.formatDuration( + mApplication.getAndroidContext(), durationInSec)); + } + return details; + } +} diff --git a/src/com/android/gallery3d/data/LocationClustering.java b/src/com/android/gallery3d/data/LocationClustering.java new file mode 100644 index 000000000..3cb1399e5 --- /dev/null +++ b/src/com/android/gallery3d/data/LocationClustering.java @@ -0,0 +1,304 @@ +/* + * 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.data; + +import com.android.gallery3d.R; +import com.android.gallery3d.util.ReverseGeocoder; +import com.android.gallery3d.util.GalleryUtils; + +import android.content.Context; +import android.widget.Toast; + +import java.util.ArrayList; + +class LocationClustering extends Clustering { + private static final String TAG = "LocationClustering"; + + private static final int MIN_GROUPS = 1; + private static final int MAX_GROUPS = 20; + private static final int MAX_ITERATIONS = 30; + + // If the total distance change is less than this ratio, stop iterating. + private static final float STOP_CHANGE_RATIO = 0.01f; + private Context mContext; + private ArrayList<ArrayList<SmallItem>> mClusters; + private ArrayList<String> mNames; + private String mNoLocationString; + + private static class Point { + public Point(double lat, double lng) { + latRad = Math.toRadians(lat); + lngRad = Math.toRadians(lng); + } + public Point() {} + public double latRad, lngRad; + } + + private static class SmallItem { + Path path; + double lat, lng; + } + + public LocationClustering(Context context) { + mContext = context; + mNoLocationString = mContext.getResources().getString(R.string.no_location); + } + + @Override + public void run(MediaSet baseSet) { + final int total = baseSet.getTotalMediaItemCount(); + final SmallItem[] buf = new SmallItem[total]; + // Separate items to two sets: with or without lat-long. + final double[] latLong = new double[2]; + baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() { + public void consume(int index, MediaItem item) { + if (index < 0 || index >= total) return; + SmallItem s = new SmallItem(); + s.path = item.getPath(); + item.getLatLong(latLong); + s.lat = latLong[0]; + s.lng = latLong[1]; + buf[index] = s; + } + }); + + final ArrayList<SmallItem> withLatLong = new ArrayList<SmallItem>(); + final ArrayList<SmallItem> withoutLatLong = new ArrayList<SmallItem>(); + final ArrayList<Point> points = new ArrayList<Point>(); + for (int i = 0; i < total; i++) { + SmallItem s = buf[i]; + if (s == null) continue; + if (GalleryUtils.isValidLocation(s.lat, s.lng)) { + withLatLong.add(s); + points.add(new Point(s.lat, s.lng)); + } else { + withoutLatLong.add(s); + } + } + + ArrayList<ArrayList<SmallItem>> clusters = new ArrayList<ArrayList<SmallItem>>(); + + int m = withLatLong.size(); + if (m > 0) { + // cluster the items with lat-long + Point[] pointsArray = new Point[m]; + pointsArray = points.toArray(pointsArray); + int[] bestK = new int[1]; + int[] index = kMeans(pointsArray, bestK); + + for (int i = 0; i < bestK[0]; i++) { + clusters.add(new ArrayList<SmallItem>()); + } + + for (int i = 0; i < m; i++) { + clusters.get(index[i]).add(withLatLong.get(i)); + } + } + + ReverseGeocoder geocoder = new ReverseGeocoder(mContext); + mNames = new ArrayList<String>(); + boolean hasUnresolvedAddress = false; + mClusters = new ArrayList<ArrayList<SmallItem>>(); + for (ArrayList<SmallItem> cluster : clusters) { + String name = generateName(cluster, geocoder); + if (name != null) { + mNames.add(name); + mClusters.add(cluster); + } else { + // move cluster-i to no location cluster + withoutLatLong.addAll(cluster); + hasUnresolvedAddress = true; + } + } + + if (withoutLatLong.size() > 0) { + mNames.add(mNoLocationString); + mClusters.add(withoutLatLong); + } + + if (hasUnresolvedAddress) { + Toast.makeText(mContext, R.string.no_connectivity, + Toast.LENGTH_LONG).show(); + } + } + + private static String generateName(ArrayList<SmallItem> items, + ReverseGeocoder geocoder) { + ReverseGeocoder.SetLatLong set = new ReverseGeocoder.SetLatLong(); + + int n = items.size(); + for (int i = 0; i < n; i++) { + SmallItem item = items.get(i); + double itemLatitude = item.lat; + double itemLongitude = item.lng; + + if (set.mMinLatLatitude > itemLatitude) { + set.mMinLatLatitude = itemLatitude; + set.mMinLatLongitude = itemLongitude; + } + if (set.mMaxLatLatitude < itemLatitude) { + set.mMaxLatLatitude = itemLatitude; + set.mMaxLatLongitude = itemLongitude; + } + if (set.mMinLonLongitude > itemLongitude) { + set.mMinLonLatitude = itemLatitude; + set.mMinLonLongitude = itemLongitude; + } + if (set.mMaxLonLongitude < itemLongitude) { + set.mMaxLonLatitude = itemLatitude; + set.mMaxLonLongitude = itemLongitude; + } + } + + return geocoder.computeAddress(set); + } + + @Override + public int getNumberOfClusters() { + return mClusters.size(); + } + + @Override + public ArrayList<Path> getCluster(int index) { + ArrayList<SmallItem> items = mClusters.get(index); + ArrayList<Path> result = new ArrayList<Path>(items.size()); + for (int i = 0, n = items.size(); i < n; i++) { + result.add(items.get(i).path); + } + return result; + } + + @Override + public String getClusterName(int index) { + return mNames.get(index); + } + + // Input: n points + // Output: the best k is stored in bestK[0], and the return value is the + // an array which specifies the group that each point belongs (0 to k - 1). + private static int[] kMeans(Point points[], int[] bestK) { + int n = points.length; + + // min and max number of groups wanted + int minK = Math.min(n, MIN_GROUPS); + int maxK = Math.min(n, MAX_GROUPS); + + Point[] center = new Point[maxK]; // center of each group. + Point[] groupSum = new Point[maxK]; // sum of points in each group. + int[] groupCount = new int[maxK]; // number of points in each group. + int[] grouping = new int[n]; // The group assignment for each point. + + for (int i = 0; i < maxK; i++) { + center[i] = new Point(); + groupSum[i] = new Point(); + } + + // The score we want to minimize is: + // (sum of distance from each point to its group center) * sqrt(k). + float bestScore = Float.MAX_VALUE; + // The best group assignment up to now. + int[] bestGrouping = new int[n]; + // The best K up to now. + bestK[0] = 1; + + float lastDistance = 0; + float totalDistance = 0; + + for (int k = minK; k <= maxK; k++) { + // step 1: (arbitrarily) pick k points as the initial centers. + int delta = n / k; + for (int i = 0; i < k; i++) { + Point p = points[i * delta]; + center[i].latRad = p.latRad; + center[i].lngRad = p.lngRad; + } + + for (int iter = 0; iter < MAX_ITERATIONS; iter++) { + // step 2: assign each point to the nearest center. + for (int i = 0; i < k; i++) { + groupSum[i].latRad = 0; + groupSum[i].lngRad = 0; + groupCount[i] = 0; + } + totalDistance = 0; + + for (int i = 0; i < n; i++) { + Point p = points[i]; + float bestDistance = Float.MAX_VALUE; + int bestIndex = 0; + for (int j = 0; j < k; j++) { + float distance = (float) GalleryUtils.fastDistanceMeters( + p.latRad, p.lngRad, center[j].latRad, center[j].lngRad); + // We may have small non-zero distance introduced by + // floating point calculation, so zero out small + // distances less than 1 meter. + if (distance < 1) { + distance = 0; + } + if (distance < bestDistance) { + bestDistance = distance; + bestIndex = j; + } + } + grouping[i] = bestIndex; + groupCount[bestIndex]++; + groupSum[bestIndex].latRad += p.latRad; + groupSum[bestIndex].lngRad += p.lngRad; + totalDistance += bestDistance; + } + + // step 3: calculate new centers + for (int i = 0; i < k; i++) { + if (groupCount[i] > 0) { + center[i].latRad = groupSum[i].latRad / groupCount[i]; + center[i].lngRad = groupSum[i].lngRad / groupCount[i]; + } + } + + if (totalDistance == 0 || (Math.abs(lastDistance - totalDistance) + / totalDistance) < STOP_CHANGE_RATIO) { + break; + } + lastDistance = totalDistance; + } + + // step 4: remove empty groups and reassign group number + int reassign[] = new int[k]; + int realK = 0; + for (int i = 0; i < k; i++) { + if (groupCount[i] > 0) { + reassign[i] = realK++; + } + } + + // step 5: calculate the final score + float score = totalDistance * (float) Math.sqrt(realK); + + if (score < bestScore) { + bestScore = score; + bestK[0] = realK; + for (int i = 0; i < n; i++) { + bestGrouping[i] = reassign[grouping[i]]; + } + if (score == 0) { + break; + } + } + } + return bestGrouping; + } +} diff --git a/src/com/android/gallery3d/data/Log.java b/src/com/android/gallery3d/data/Log.java new file mode 100644 index 000000000..3384eb66c --- /dev/null +++ b/src/com/android/gallery3d/data/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.data; + +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/data/MediaDetails.java b/src/com/android/gallery3d/data/MediaDetails.java new file mode 100644 index 000000000..1b56ac42e --- /dev/null +++ b/src/com/android/gallery3d/data/MediaDetails.java @@ -0,0 +1,162 @@ +/* + * 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.data; + +import com.android.gallery3d.R; + +import android.media.ExifInterface; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.TreeMap; +import java.util.Map.Entry; + +public class MediaDetails implements Iterable<Entry<Integer, Object>> { + @SuppressWarnings("unused") + private static final String TAG = "MediaDetails"; + + private TreeMap<Integer, Object> mDetails = new TreeMap<Integer, Object>(); + private HashMap<Integer, Integer> mUnits = new HashMap<Integer, Integer>(); + + public static final int INDEX_TITLE = 1; + public static final int INDEX_DESCRIPTION = 2; + public static final int INDEX_DATETIME = 3; + public static final int INDEX_LOCATION = 4; + public static final int INDEX_WIDTH = 5; + public static final int INDEX_HEIGHT = 6; + public static final int INDEX_ORIENTATION = 7; + public static final int INDEX_DURATION = 8; + public static final int INDEX_MIMETYPE = 9; + public static final int INDEX_SIZE = 10; + + // for EXIF + public static final int INDEX_MAKE = 100; + public static final int INDEX_MODEL = 101; + public static final int INDEX_FLASH = 102; + public static final int INDEX_FOCAL_LENGTH = 103; + public static final int INDEX_WHITE_BALANCE = 104; + public static final int INDEX_APERTURE = 105; + public static final int INDEX_SHUTTER_SPEED = 106; + public static final int INDEX_EXPOSURE_TIME = 107; + public static final int INDEX_ISO = 108; + + // Put this last because it may be long. + public static final int INDEX_PATH = 200; + + public static class FlashState { + private static int FLASH_FIRED_MASK = 1; + private static int FLASH_RETURN_MASK = 2 | 4; + private static int FLASH_MODE_MASK = 8 | 16; + private static int FLASH_FUNCTION_MASK = 32; + private static int FLASH_RED_EYE_MASK = 64; + private int mState; + + public FlashState(int state) { + mState = state; + } + + public boolean isFlashFired() { + return (mState & FLASH_FIRED_MASK) != 0; + } + + public int getFlashReturn() { + return (mState & FLASH_RETURN_MASK) >> 1; + } + + public int getFlashMode() { + return (mState & FLASH_MODE_MASK) >> 3; + } + + public boolean isFlashPresent() { + return (mState & FLASH_FUNCTION_MASK) != 0; + } + + public boolean isRedEyeModePresent() { + return (mState & FLASH_RED_EYE_MASK) != 0; + } + } + + public void addDetail(int index, Object value) { + mDetails.put(index, value); + } + + public Object getDetail(int index) { + return mDetails.get(index); + } + + public int size() { + return mDetails.size(); + } + + public Iterator<Entry<Integer, Object>> iterator() { + return mDetails.entrySet().iterator(); + } + + public void setUnit(int index, int unit) { + mUnits.put(index, unit); + } + + public boolean hasUnit(int index) { + return mUnits.containsKey(index); + } + + public int getUnit(int index) { + return mUnits.get(index); + } + + private static void setExifData(MediaDetails details, ExifInterface exif, String tag, + int key) { + String value = exif.getAttribute(tag); + if (value != null) { + if (key == MediaDetails.INDEX_FLASH) { + MediaDetails.FlashState state = new MediaDetails.FlashState( + Integer.valueOf(value.toString())); + details.addDetail(key, state); + } else { + details.addDetail(key, value); + } + } + } + + public static void extractExifInfo(MediaDetails details, String filePath) { + try { + ExifInterface exif = new ExifInterface(filePath); + setExifData(details, exif, ExifInterface.TAG_FLASH, MediaDetails.INDEX_FLASH); + setExifData(details, exif, ExifInterface.TAG_IMAGE_WIDTH, MediaDetails.INDEX_WIDTH); + setExifData(details, exif, ExifInterface.TAG_IMAGE_LENGTH, + MediaDetails.INDEX_HEIGHT); + setExifData(details, exif, ExifInterface.TAG_MAKE, MediaDetails.INDEX_MAKE); + setExifData(details, exif, ExifInterface.TAG_MODEL, MediaDetails.INDEX_MODEL); + setExifData(details, exif, ExifInterface.TAG_APERTURE, MediaDetails.INDEX_APERTURE); + setExifData(details, exif, ExifInterface.TAG_ISO, MediaDetails.INDEX_ISO); + setExifData(details, exif, ExifInterface.TAG_WHITE_BALANCE, + MediaDetails.INDEX_WHITE_BALANCE); + setExifData(details, exif, ExifInterface.TAG_EXPOSURE_TIME, + MediaDetails.INDEX_EXPOSURE_TIME); + + double data = exif.getAttributeDouble(ExifInterface.TAG_FOCAL_LENGTH, 0); + if (data != 0f) { + details.addDetail(MediaDetails.INDEX_FOCAL_LENGTH, data); + details.setUnit(MediaDetails.INDEX_FOCAL_LENGTH, R.string.unit_mm); + } + } catch (IOException ex) { + // ignore it. + Log.w(TAG, "", ex); + } + } +} diff --git a/src/com/android/gallery3d/data/MediaItem.java b/src/com/android/gallery3d/data/MediaItem.java new file mode 100644 index 000000000..430d8327d --- /dev/null +++ b/src/com/android/gallery3d/data/MediaItem.java @@ -0,0 +1,75 @@ +/* + * 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.data; + +import com.android.gallery3d.util.ThreadPool.Job; + +import android.graphics.Bitmap; +import android.graphics.BitmapRegionDecoder; + +// MediaItem represents an image or a video item. +public abstract class MediaItem extends MediaObject { + // NOTE: These type numbers are stored in the image cache, so it should not + // not be changed without resetting the cache. + public static final int TYPE_THUMBNAIL = 1; + public static final int TYPE_MICROTHUMBNAIL = 2; + + public static final int IMAGE_READY = 0; + public static final int IMAGE_WAIT = 1; + public static final int IMAGE_ERROR = -1; + + // TODO: fix default value for latlng and change this. + public static final double INVALID_LATLNG = 0f; + + public abstract Job<Bitmap> requestImage(int type); + public abstract Job<BitmapRegionDecoder> requestLargeImage(); + + public MediaItem(Path path, long version) { + super(path, version); + } + + public long getDateInMs() { + return 0; + } + + public String getName() { + return null; + } + + public void getLatLong(double[] latLong) { + latLong[0] = INVALID_LATLNG; + latLong[1] = INVALID_LATLNG; + } + + public String[] getTags() { + return null; + } + + public Face[] getFaces() { + return null; + } + + public int getRotation() { + return 0; + } + + public long getSize() { + return 0; + } + + public abstract String getMimeType(); +} diff --git a/src/com/android/gallery3d/data/MediaObject.java b/src/com/android/gallery3d/data/MediaObject.java new file mode 100644 index 000000000..d0f1672fc --- /dev/null +++ b/src/com/android/gallery3d/data/MediaObject.java @@ -0,0 +1,130 @@ +/* + * 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.data; + +import android.net.Uri; + +public abstract class MediaObject { + @SuppressWarnings("unused") + private static final String TAG = "MediaObject"; + public static final long INVALID_DATA_VERSION = -1; + + // These are the bits returned from getSupportedOperations(): + public static final int SUPPORT_DELETE = 1 << 0; + public static final int SUPPORT_ROTATE = 1 << 1; + public static final int SUPPORT_SHARE = 1 << 2; + public static final int SUPPORT_CROP = 1 << 3; + public static final int SUPPORT_SHOW_ON_MAP = 1 << 4; + public static final int SUPPORT_SETAS = 1 << 5; + public static final int SUPPORT_FULL_IMAGE = 1 << 6; + public static final int SUPPORT_PLAY = 1 << 7; + public static final int SUPPORT_CACHE = 1 << 8; + public static final int SUPPORT_EDIT = 1 << 9; + public static final int SUPPORT_INFO = 1 << 10; + public static final int SUPPORT_IMPORT = 1 << 11; + public static final int SUPPORT_ALL = 0xffffffff; + + // These are the bits returned from getMediaType(): + public static final int MEDIA_TYPE_UNKNOWN = 1; + public static final int MEDIA_TYPE_IMAGE = 2; + public static final int MEDIA_TYPE_VIDEO = 4; + public static final int MEDIA_TYPE_ALL = MEDIA_TYPE_IMAGE | MEDIA_TYPE_VIDEO; + + // These are flags for cache() and return values for getCacheFlag(): + public static final int CACHE_FLAG_NO = 0; + public static final int CACHE_FLAG_SCREENNAIL = 1; + public static final int CACHE_FLAG_FULL = 2; + + // These are return values for getCacheStatus(): + public static final int CACHE_STATUS_NOT_CACHED = 0; + public static final int CACHE_STATUS_CACHING = 1; + public static final int CACHE_STATUS_CACHED_SCREENNAIL = 2; + public static final int CACHE_STATUS_CACHED_FULL = 3; + + private static long sVersionSerial = 0; + + protected long mDataVersion; + + protected final Path mPath; + + public MediaObject(Path path, long version) { + path.setObject(this); + mPath = path; + mDataVersion = version; + } + + public Path getPath() { + return mPath; + } + + public int getSupportedOperations() { + return 0; + } + + public void delete() { + throw new UnsupportedOperationException(); + } + + public void rotate(int degrees) { + throw new UnsupportedOperationException(); + } + + public Uri getContentUri() { + throw new UnsupportedOperationException(); + } + + public Uri getPlayUri() { + throw new UnsupportedOperationException(); + } + + public int getMediaType() { + return MEDIA_TYPE_UNKNOWN; + } + + public boolean Import() { + throw new UnsupportedOperationException(); + } + + public MediaDetails getDetails() { + MediaDetails details = new MediaDetails(); + return details; + } + + public long getDataVersion() { + return mDataVersion; + } + + public int getCacheFlag() { + return CACHE_FLAG_NO; + } + + public int getCacheStatus() { + throw new UnsupportedOperationException(); + } + + public long getCacheSize() { + throw new UnsupportedOperationException(); + } + + public void cache(int flag) { + throw new UnsupportedOperationException(); + } + + public static synchronized long nextVersionNumber() { + return ++MediaObject.sVersionSerial; + } +} diff --git a/src/com/android/gallery3d/data/MediaSet.java b/src/com/android/gallery3d/data/MediaSet.java new file mode 100644 index 000000000..99f00a0dd --- /dev/null +++ b/src/com/android/gallery3d/data/MediaSet.java @@ -0,0 +1,219 @@ +/* + * 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.data; + +import com.android.gallery3d.util.Future; + +import java.util.ArrayList; +import java.util.WeakHashMap; + +// MediaSet is a directory-like data structure. +// It contains MediaItems and sub-MediaSets. +// +// The primary interface are: +// getMediaItemCount(), getMediaItem() and +// getSubMediaSetCount(), getSubMediaSet(). +// +// getTotalMediaItemCount() returns the number of all MediaItems, including +// those in sub-MediaSets. +public abstract class MediaSet extends MediaObject { + public static final int MEDIAITEM_BATCH_FETCH_COUNT = 500; + public static final int INDEX_NOT_FOUND = -1; + + public MediaSet(Path path, long version) { + super(path, version); + } + + public int getMediaItemCount() { + return 0; + } + + // Returns the media items in the range [start, start + count). + // + // The number of media items returned may be less than the specified count + // if there are not enough media items available. The number of + // media items available may not be consistent with the return value of + // getMediaItemCount() because the contents of database may have already + // changed. + public ArrayList<MediaItem> getMediaItem(int start, int count) { + return new ArrayList<MediaItem>(); + } + + public int getSubMediaSetCount() { + return 0; + } + + public MediaSet getSubMediaSet(int index) { + throw new IndexOutOfBoundsException(); + } + + public boolean isLeafAlbum() { + return false; + } + + public int getTotalMediaItemCount() { + int total = getMediaItemCount(); + for (int i = 0, n = getSubMediaSetCount(); i < n; i++) { + total += getSubMediaSet(i).getTotalMediaItemCount(); + } + return total; + } + + // TODO: we should have better implementation of sub classes + public int getIndexOfItem(Path path, int hint) { + // hint < 0 is handled below + // first, try to find it around the hint + int start = Math.max(0, + hint - MEDIAITEM_BATCH_FETCH_COUNT / 2); + ArrayList<MediaItem> list = getMediaItem( + start, MEDIAITEM_BATCH_FETCH_COUNT); + int index = getIndexOf(path, list); + if (index != INDEX_NOT_FOUND) return start + index; + + // try to find it globally + start = start == 0 ? MEDIAITEM_BATCH_FETCH_COUNT : 0; + list = getMediaItem(start, MEDIAITEM_BATCH_FETCH_COUNT); + while (true) { + index = getIndexOf(path, list); + if (index != INDEX_NOT_FOUND) return start + index; + if (list.size() < MEDIAITEM_BATCH_FETCH_COUNT) return INDEX_NOT_FOUND; + start += MEDIAITEM_BATCH_FETCH_COUNT; + list = getMediaItem(start, MEDIAITEM_BATCH_FETCH_COUNT); + } + } + + protected int getIndexOf(Path path, ArrayList<MediaItem> list) { + for (int i = 0, n = list.size(); i < n; ++i) { + if (list.get(i).mPath == path) return i; + } + return INDEX_NOT_FOUND; + } + + public abstract String getName(); + + private WeakHashMap<ContentListener, Object> mListeners = + new WeakHashMap<ContentListener, Object>(); + + // NOTE: The MediaSet only keeps a weak reference to the listener. The + // listener is automatically removed when there is no other reference to + // the listener. + public void addContentListener(ContentListener listener) { + if (mListeners.containsKey(listener)) { + throw new IllegalArgumentException(); + } + mListeners.put(listener, null); + } + + public void removeContentListener(ContentListener listener) { + if (!mListeners.containsKey(listener)) { + throw new IllegalArgumentException(); + } + mListeners.remove(listener); + } + + // This should be called by subclasses when the content is changed. + public void notifyContentChanged() { + for (ContentListener listener : mListeners.keySet()) { + listener.onContentDirty(); + } + } + + // Reload the content. Return the current data version. reload() should be called + // in the same thread as getMediaItem(int, int) and getSubMediaSet(int). + public abstract long reload(); + + @Override + public MediaDetails getDetails() { + MediaDetails details = super.getDetails(); + details.addDetail(MediaDetails.INDEX_TITLE, getName()); + return details; + } + + // Enumerate all media items in this media set (including the ones in sub + // media sets), in an efficient order. ItemConsumer.consumer() will be + // called for each media item with its index. + public void enumerateMediaItems(ItemConsumer consumer) { + enumerateMediaItems(consumer, 0); + } + + public void enumerateTotalMediaItems(ItemConsumer consumer) { + enumerateTotalMediaItems(consumer, 0); + } + + public static interface ItemConsumer { + void consume(int index, MediaItem item); + } + + // The default implementation uses getMediaItem() for enumerateMediaItems(). + // Subclasses may override this and use more efficient implementations. + // Returns the number of items enumerated. + protected int enumerateMediaItems(ItemConsumer consumer, int startIndex) { + int total = getMediaItemCount(); + int start = 0; + while (start < total) { + int count = Math.min(MEDIAITEM_BATCH_FETCH_COUNT, total - start); + ArrayList<MediaItem> items = getMediaItem(start, count); + for (int i = 0, n = items.size(); i < n; i++) { + MediaItem item = items.get(i); + consumer.consume(startIndex + start + i, item); + } + start += count; + } + return total; + } + + // Recursively enumerate all media items under this set. + // Returns the number of items enumerated. + protected int enumerateTotalMediaItems( + ItemConsumer consumer, int startIndex) { + int start = 0; + start += enumerateMediaItems(consumer, startIndex); + int m = getSubMediaSetCount(); + for (int i = 0; i < m; i++) { + start += getSubMediaSet(i).enumerateTotalMediaItems( + consumer, startIndex + start); + } + return start; + } + + public Future<Void> requestSync() { + return FUTURE_STUB; + } + + private static final Future<Void> FUTURE_STUB = new Future<Void>() { + @Override + public void cancel() {} + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public Void get() { + return null; + } + + @Override + public void waitDone() {} + }; +} diff --git a/src/com/android/gallery3d/data/MediaSource.java b/src/com/android/gallery3d/data/MediaSource.java new file mode 100644 index 000000000..ae98e0fcc --- /dev/null +++ b/src/com/android/gallery3d/data/MediaSource.java @@ -0,0 +1,93 @@ +/* + * 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.data; + +import com.android.gallery3d.data.MediaSet.ItemConsumer; + +import android.net.Uri; + +import java.util.ArrayList; + +public abstract class MediaSource { + private static final String TAG = "MediaSource"; + private String mPrefix; + + protected MediaSource(String prefix) { + mPrefix = prefix; + } + + public String getPrefix() { + return mPrefix; + } + + public Path findPathByUri(Uri uri) { + return null; + } + + public abstract MediaObject createMediaObject(Path path); + + public void pause() { + } + + public void resume() { + } + + public Path getDefaultSetOf(Path item) { + return null; + } + + public long getTotalUsedCacheSize() { + return 0; + } + + public long getTotalTargetCacheSize() { + return 0; + } + + public static class PathId { + public PathId(Path path, int id) { + this.path = path; + this.id = id; + } + public Path path; + public int id; + } + + // Maps a list of Paths (all belong to this MediaSource) to MediaItems, + // and invoke consumer.consume() for each MediaItem with the given id. + // + // This default implementation uses getMediaObject for each Path. Subclasses + // may override this and provide more efficient implementation (like + // batching the database query). + public void mapMediaItems(ArrayList<PathId> list, ItemConsumer consumer) { + int n = list.size(); + for (int i = 0; i < n; i++) { + PathId pid = list.get(i); + MediaObject obj = pid.path.getObject(); + if (obj == null) { + try { + obj = createMediaObject(pid.path); + } catch (Throwable th) { + Log.w(TAG, "cannot create media object: " + pid.path, th); + } + } + if (obj != null) { + consumer.consume(pid.id, (MediaItem) obj); + } + } + } +} diff --git a/src/com/android/gallery3d/data/MtpClient.java b/src/com/android/gallery3d/data/MtpClient.java new file mode 100644 index 000000000..6991c1637 --- /dev/null +++ b/src/com/android/gallery3d/data/MtpClient.java @@ -0,0 +1,442 @@ +/* + * 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.data; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.usb.UsbConstants; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbInterface; +import android.hardware.usb.UsbManager; +import android.mtp.MtpDevice; +import android.mtp.MtpDeviceInfo; +import android.mtp.MtpObjectInfo; +import android.mtp.MtpStorageInfo; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * This class helps an application manage a list of connected MTP or PTP devices. + * It listens for MTP devices being attached and removed from the USB host bus + * and notifies the application when the MTP device list changes. + */ +public class MtpClient { + + private static final String TAG = "MtpClient"; + + private static final String ACTION_USB_PERMISSION = + "android.mtp.MtpClient.action.USB_PERMISSION"; + + private final Context mContext; + private final UsbManager mUsbManager; + private final ArrayList<Listener> mListeners = new ArrayList<Listener>(); + // mDevices contains all MtpDevices that have been seen by our client, + // so we can inform when the device has been detached. + // mDevices is also used for synchronization in this class. + private final HashMap<String, MtpDevice> mDevices = new HashMap<String, MtpDevice>(); + // List of MTP devices we should not try to open for which we are currently + // asking for permission to open. + private final ArrayList<String> mRequestPermissionDevices = new ArrayList<String>(); + // List of MTP devices we should not try to open. + // We add devices to this list if the user canceled a permission request or we were + // unable to open the device. + private final ArrayList<String> mIgnoredDevices = new ArrayList<String>(); + + private final PendingIntent mPermissionIntent; + + private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + UsbDevice usbDevice = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + String deviceName = usbDevice.getDeviceName(); + + synchronized (mDevices) { + MtpDevice mtpDevice = mDevices.get(deviceName); + + if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) { + if (mtpDevice == null) { + mtpDevice = openDeviceLocked(usbDevice); + } + if (mtpDevice != null) { + for (Listener listener : mListeners) { + listener.deviceAdded(mtpDevice); + } + } + } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) { + if (mtpDevice != null) { + mDevices.remove(deviceName); + mRequestPermissionDevices.remove(deviceName); + mIgnoredDevices.remove(deviceName); + for (Listener listener : mListeners) { + listener.deviceRemoved(mtpDevice); + } + } + } else if (ACTION_USB_PERMISSION.equals(action)) { + mRequestPermissionDevices.remove(deviceName); + boolean permission = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, + false); + Log.d(TAG, "ACTION_USB_PERMISSION: " + permission); + if (permission) { + if (mtpDevice == null) { + mtpDevice = openDeviceLocked(usbDevice); + } + if (mtpDevice != null) { + for (Listener listener : mListeners) { + listener.deviceAdded(mtpDevice); + } + } + } else { + // so we don't ask for permission again + mIgnoredDevices.add(deviceName); + } + } + } + } + }; + + /** + * An interface for being notified when MTP or PTP devices are attached + * or removed. In the current implementation, only PTP devices are supported. + */ + public interface Listener { + /** + * Called when a new device has been added + * + * @param device the new device that was added + */ + public void deviceAdded(MtpDevice device); + + /** + * Called when a new device has been removed + * + * @param device the device that was removed + */ + public void deviceRemoved(MtpDevice device); + } + + /** + * Tests to see if a {@link android.hardware.usb.UsbDevice} + * supports the PTP protocol (typically used by digital cameras) + * + * @param device the device to test + * @return true if the device is a PTP device. + */ + static public boolean isCamera(UsbDevice device) { + int count = device.getInterfaceCount(); + for (int i = 0; i < count; i++) { + UsbInterface intf = device.getInterface(i); + if (intf.getInterfaceClass() == UsbConstants.USB_CLASS_STILL_IMAGE && + intf.getInterfaceSubclass() == 1 && + intf.getInterfaceProtocol() == 1) { + return true; + } + } + return false; + } + + /** + * MtpClient constructor + * + * @param context the {@link android.content.Context} to use for the MtpClient + */ + public MtpClient(Context context) { + mContext = context; + mUsbManager = (UsbManager)context.getSystemService(Context.USB_SERVICE); + mPermissionIntent = PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_USB_PERMISSION), 0); + IntentFilter filter = new IntentFilter(); + filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); + filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); + filter.addAction(ACTION_USB_PERMISSION); + context.registerReceiver(mUsbReceiver, filter); + } + + /** + * Opens the {@link android.hardware.usb.UsbDevice} for an MTP or PTP + * device and return an {@link android.mtp.MtpDevice} for it. + * + * @param device the device to open + * @return an MtpDevice for the device. + */ + private MtpDevice openDeviceLocked(UsbDevice usbDevice) { + String deviceName = usbDevice.getDeviceName(); + + // don't try to open devices that we have decided to ignore + // or are currently asking permission for + if (isCamera(usbDevice) && !mIgnoredDevices.contains(deviceName) + && !mRequestPermissionDevices.contains(deviceName)) { + if (!mUsbManager.hasPermission(usbDevice)) { + mUsbManager.requestPermission(usbDevice, mPermissionIntent); + mRequestPermissionDevices.add(deviceName); + } else { + UsbDeviceConnection connection = mUsbManager.openDevice(usbDevice); + if (connection != null) { + MtpDevice mtpDevice = new MtpDevice(usbDevice); + if (mtpDevice.open(connection)) { + mDevices.put(usbDevice.getDeviceName(), mtpDevice); + return mtpDevice; + } else { + // so we don't try to open it again + mIgnoredDevices.add(deviceName); + } + } else { + // so we don't try to open it again + mIgnoredDevices.add(deviceName); + } + } + } + return null; + } + + /** + * Closes all resources related to the MtpClient object + */ + public void close() { + mContext.unregisterReceiver(mUsbReceiver); + } + + /** + * Registers a {@link android.mtp.MtpClient.Listener} interface to receive + * notifications when MTP or PTP devices are added or removed. + * + * @param listener the listener to register + */ + public void addListener(Listener listener) { + synchronized (mDevices) { + if (!mListeners.contains(listener)) { + mListeners.add(listener); + } + } + } + + /** + * Unregisters a {@link android.mtp.MtpClient.Listener} interface. + * + * @param listener the listener to unregister + */ + public void removeListener(Listener listener) { + synchronized (mDevices) { + mListeners.remove(listener); + } + } + + /** + * Retrieves an {@link android.mtp.MtpDevice} object for the USB device + * with the given name. + * + * @param deviceName the name of the USB device + * @return the MtpDevice, or null if it does not exist + */ + public MtpDevice getDevice(String deviceName) { + synchronized (mDevices) { + return mDevices.get(deviceName); + } + } + + /** + * Retrieves an {@link android.mtp.MtpDevice} object for the USB device + * with the given ID. + * + * @param id the ID of the USB device + * @return the MtpDevice, or null if it does not exist + */ + public MtpDevice getDevice(int id) { + synchronized (mDevices) { + return mDevices.get(UsbDevice.getDeviceName(id)); + } + } + + /** + * Retrieves a list of all currently connected {@link android.mtp.MtpDevice}. + * + * @return the list of MtpDevices + */ + public List<MtpDevice> getDeviceList() { + synchronized (mDevices) { + // Query the USB manager since devices might have attached + // before we added our listener. + for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) { + if (mDevices.get(usbDevice.getDeviceName()) == null) { + openDeviceLocked(usbDevice); + } + } + + return new ArrayList<MtpDevice>(mDevices.values()); + } + } + + /** + * Retrieves a list of all {@link android.mtp.MtpStorageInfo} + * for the MTP or PTP device with the given USB device name + * + * @param deviceName the name of the USB device + * @return the list of MtpStorageInfo + */ + public List<MtpStorageInfo> getStorageList(String deviceName) { + MtpDevice device = getDevice(deviceName); + if (device == null) { + return null; + } + int[] storageIds = device.getStorageIds(); + if (storageIds == null) { + return null; + } + + int length = storageIds.length; + ArrayList<MtpStorageInfo> storageList = new ArrayList<MtpStorageInfo>(length); + for (int i = 0; i < length; i++) { + MtpStorageInfo info = device.getStorageInfo(storageIds[i]); + if (info == null) { + Log.w(TAG, "getStorageInfo failed"); + } else { + storageList.add(info); + } + } + return storageList; + } + + /** + * Retrieves the {@link android.mtp.MtpObjectInfo} for an object on + * the MTP or PTP device with the given USB device name with the given + * object handle + * + * @param deviceName the name of the USB device + * @param objectHandle handle of the object to query + * @return the MtpObjectInfo + */ + public MtpObjectInfo getObjectInfo(String deviceName, int objectHandle) { + MtpDevice device = getDevice(deviceName); + if (device == null) { + return null; + } + return device.getObjectInfo(objectHandle); + } + + /** + * Deletes an object on the MTP or PTP device with the given USB device name. + * + * @param deviceName the name of the USB device + * @param objectHandle handle of the object to delete + * @return true if the deletion succeeds + */ + public boolean deleteObject(String deviceName, int objectHandle) { + MtpDevice device = getDevice(deviceName); + if (device == null) { + return false; + } + return device.deleteObject(objectHandle); + } + + /** + * Retrieves a list of {@link android.mtp.MtpObjectInfo} for all objects + * on the MTP or PTP device with the given USB device name and given storage ID + * and/or object handle. + * If the object handle is zero, then all objects in the root of the storage unit + * will be returned. Otherwise, all immediate children of the object will be returned. + * If the storage ID is also zero, then all objects on all storage units will be returned. + * + * @param deviceName the name of the USB device + * @param storageId the ID of the storage unit to query, or zero for all + * @param objectHandle the handle of the parent object to query, or zero for the storage root + * @return the list of MtpObjectInfo + */ + public List<MtpObjectInfo> getObjectList(String deviceName, int storageId, int objectHandle) { + MtpDevice device = getDevice(deviceName); + if (device == null) { + return null; + } + if (objectHandle == 0) { + // all objects in root of storage + objectHandle = 0xFFFFFFFF; + } + int[] handles = device.getObjectHandles(storageId, 0, objectHandle); + if (handles == null) { + return null; + } + + int length = handles.length; + ArrayList<MtpObjectInfo> objectList = new ArrayList<MtpObjectInfo>(length); + for (int i = 0; i < length; i++) { + MtpObjectInfo info = device.getObjectInfo(handles[i]); + if (info == null) { + Log.w(TAG, "getObjectInfo failed"); + } else { + objectList.add(info); + } + } + return objectList; + } + + /** + * Returns the data for an object as a byte array. + * + * @param deviceName the name of the USB device containing the object + * @param objectHandle handle of the object to read + * @param objectSize the size of the object (this should match + * {@link android.mtp.MtpObjectInfo#getCompressedSize} + * @return the object's data, or null if reading fails + */ + public byte[] getObject(String deviceName, int objectHandle, int objectSize) { + MtpDevice device = getDevice(deviceName); + if (device == null) { + return null; + } + return device.getObject(objectHandle, objectSize); + } + + /** + * Returns the thumbnail data for an object as a byte array. + * + * @param deviceName the name of the USB device containing the object + * @param objectHandle handle of the object to read + * @return the object's thumbnail, or null if reading fails + */ + public byte[] getThumbnail(String deviceName, int objectHandle) { + MtpDevice device = getDevice(deviceName); + if (device == null) { + return null; + } + return device.getThumbnail(objectHandle); + } + + /** + * Copies the data for an object to a file in external storage. + * + * @param deviceName the name of the USB device containing the object + * @param objectHandle handle of the object to read + * @param destPath path to destination for the file transfer. + * This path should be in the external storage as defined by + * {@link android.os.Environment#getExternalStorageDirectory} + * @return true if the file transfer succeeds + */ + public boolean importFile(String deviceName, int objectHandle, String destPath) { + MtpDevice device = getDevice(deviceName); + if (device == null) { + return false; + } + return device.importFile(objectHandle, destPath); + } +} diff --git a/src/com/android/gallery3d/data/MtpContext.java b/src/com/android/gallery3d/data/MtpContext.java new file mode 100644 index 000000000..652849445 --- /dev/null +++ b/src/com/android/gallery3d/data/MtpContext.java @@ -0,0 +1,141 @@ +package com.android.gallery3d.data; + +import com.android.gallery3d.R; +import com.android.gallery3d.util.GalleryUtils; + +import android.content.Context; +import android.hardware.usb.UsbDevice; +import android.media.MediaScannerConnection; +import android.media.MediaScannerConnection.MediaScannerConnectionClient; +import android.mtp.MtpObjectInfo; +import android.net.Uri; +import android.os.Environment; +import android.util.Log; +import android.widget.Toast; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class MtpContext implements MtpClient.Listener { + private static final String TAG = "MtpContext"; + + public static final String NAME_IMPORTED_FOLDER = "Imported"; + + private ScannerClient mScannerClient; + private Context mContext; + private MtpClient mClient; + + private static final class ScannerClient implements MediaScannerConnectionClient { + ArrayList<String> mPaths = new ArrayList<String>(); + MediaScannerConnection mScannerConnection; + boolean mConnected; + Object mLock = new Object(); + + public ScannerClient(Context context) { + mScannerConnection = new MediaScannerConnection(context, this); + } + + public void scanPath(String path) { + synchronized (mLock) { + if (mConnected) { + mScannerConnection.scanFile(path, null); + } else { + mPaths.add(path); + mScannerConnection.connect(); + } + } + } + + @Override + public void onMediaScannerConnected() { + synchronized (mLock) { + mConnected = true; + if (!mPaths.isEmpty()) { + for (String path : mPaths) { + mScannerConnection.scanFile(path, null); + } + mPaths.clear(); + } + } + } + + @Override + public void onScanCompleted(String path, Uri uri) { + } + } + + public MtpContext(Context context) { + mContext = context; + mScannerClient = new ScannerClient(context); + mClient = new MtpClient(mContext); + } + + public void pause() { + mClient.removeListener(this); + } + + public void resume() { + mClient.addListener(this); + notifyDirty(); + } + + public void deviceAdded(android.mtp.MtpDevice device) { + notifyDirty(); + showToast(R.string.camera_connected); + } + + public void deviceRemoved(android.mtp.MtpDevice device) { + notifyDirty(); + showToast(R.string.camera_disconnected); + } + + private void notifyDirty() { + mContext.getContentResolver().notifyChange(Uri.parse("mtp://"), null); + } + + private void showToast(final int msg) { + Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show(); + } + + public MtpClient getMtpClient() { + return mClient; + } + + public boolean copyFile(String deviceName, MtpObjectInfo objInfo) { + if (GalleryUtils.hasSpaceForSize(objInfo.getCompressedSize())) { + File dest = Environment.getExternalStorageDirectory(); + dest = new File(dest, NAME_IMPORTED_FOLDER); + dest.mkdirs(); + String destPath = new File(dest, objInfo.getName()).getAbsolutePath(); + int objectId = objInfo.getObjectHandle(); + if (mClient.importFile(deviceName, objectId, destPath)) { + mScannerClient.scanPath(destPath); + return true; + } + } else { + Log.w(TAG, "No space to import " + objInfo.getName() + + " whose size = " + objInfo.getCompressedSize()); + } + return false; + } + + public boolean copyAlbum(String deviceName, String albumName, + List<MtpObjectInfo> children) { + File dest = Environment.getExternalStorageDirectory(); + dest = new File(dest, albumName); + dest.mkdirs(); + int success = 0; + for (MtpObjectInfo child : children) { + if (!GalleryUtils.hasSpaceForSize(child.getCompressedSize())) continue; + + File importedFile = new File(dest, child.getName()); + String path = importedFile.getAbsolutePath(); + if (mClient.importFile(deviceName, child.getObjectHandle(), path)) { + mScannerClient.scanPath(path); + success++; + } + } + return success == children.size(); + } +} diff --git a/src/com/android/gallery3d/data/MtpDevice.java b/src/com/android/gallery3d/data/MtpDevice.java new file mode 100644 index 000000000..e654583c5 --- /dev/null +++ b/src/com/android/gallery3d/data/MtpDevice.java @@ -0,0 +1,174 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; + +import android.hardware.usb.UsbDevice; +import android.mtp.MtpConstants; +import android.mtp.MtpObjectInfo; +import android.mtp.MtpStorageInfo; +import android.net.Uri; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +public class MtpDevice extends MediaSet { + private static final String TAG = "MtpDevice"; + + private final GalleryApp mApplication; + private final int mDeviceId; + private final String mDeviceName; + private final DataManager mDataManager; + private final MtpContext mMtpContext; + private final String mName; + private final ChangeNotifier mNotifier; + private final Path mItemPath; + private List<MtpObjectInfo> mJpegChildren; + + public MtpDevice(Path path, GalleryApp application, int deviceId, + String name, MtpContext mtpContext) { + super(path, nextVersionNumber()); + mApplication = application; + mDeviceId = deviceId; + mDeviceName = UsbDevice.getDeviceName(deviceId); + mDataManager = application.getDataManager(); + mMtpContext = mtpContext; + mName = name; + mNotifier = new ChangeNotifier(this, Uri.parse("mtp://"), application); + mItemPath = Path.fromString("/mtp/item/" + String.valueOf(deviceId)); + mJpegChildren = new ArrayList<MtpObjectInfo>(); + } + + public MtpDevice(Path path, GalleryApp application, int deviceId, + MtpContext mtpContext) { + this(path, application, deviceId, + MtpDeviceSet.getDeviceName(mtpContext, deviceId), mtpContext); + } + + private List<MtpObjectInfo> loadItems() { + ArrayList<MtpObjectInfo> result = new ArrayList<MtpObjectInfo>(); + + List<MtpStorageInfo> storageList = mMtpContext.getMtpClient() + .getStorageList(mDeviceName); + if (storageList == null) return result; + + for (MtpStorageInfo info : storageList) { + collectJpegChildren(info.getStorageId(), 0, result); + } + + return result; + } + + private void collectJpegChildren(int storageId, int objectId, + ArrayList<MtpObjectInfo> result) { + ArrayList<MtpObjectInfo> dirChildren = new ArrayList<MtpObjectInfo>(); + + queryChildren(storageId, objectId, result, dirChildren); + + for (int i = 0, n = dirChildren.size(); i < n; i++) { + MtpObjectInfo info = dirChildren.get(i); + collectJpegChildren(storageId, info.getObjectHandle(), result); + } + } + + private void queryChildren(int storageId, int objectId, + ArrayList<MtpObjectInfo> jpeg, ArrayList<MtpObjectInfo> dir) { + List<MtpObjectInfo> children = mMtpContext.getMtpClient().getObjectList( + mDeviceName, storageId, objectId); + if (children == null) return; + + for (MtpObjectInfo obj : children) { + int format = obj.getFormat(); + switch (format) { + case MtpConstants.FORMAT_JFIF: + case MtpConstants.FORMAT_EXIF_JPEG: + jpeg.add(obj); + break; + case MtpConstants.FORMAT_ASSOCIATION: + dir.add(obj); + break; + default: + Log.w(TAG, "other type: name = " + obj.getName() + + ", format = " + format); + } + } + } + + public static MtpObjectInfo getObjectInfo(MtpContext mtpContext, int deviceId, + int objectId) { + String deviceName = UsbDevice.getDeviceName(deviceId); + return mtpContext.getMtpClient().getObjectInfo(deviceName, objectId); + } + + @Override + public ArrayList<MediaItem> getMediaItem(int start, int count) { + ArrayList<MediaItem> result = new ArrayList<MediaItem>(); + int begin = start; + int end = Math.min(start + count, mJpegChildren.size()); + + DataManager dataManager = mApplication.getDataManager(); + for (int i = begin; i < end; i++) { + MtpObjectInfo child = mJpegChildren.get(i); + Path childPath = mItemPath.getChild(child.getObjectHandle()); + MtpImage image = (MtpImage) dataManager.peekMediaObject(childPath); + if (image == null) { + image = new MtpImage( + childPath, mApplication, mDeviceId, child, mMtpContext); + } else { + image.updateContent(child); + } + result.add(image); + } + return result; + } + + @Override + public int getMediaItemCount() { + return mJpegChildren.size(); + } + + @Override + public String getName() { + return mName; + } + + @Override + public long reload() { + if (mNotifier.isDirty()) { + mDataVersion = nextVersionNumber(); + mJpegChildren = loadItems(); + } + return mDataVersion; + } + + @Override + public int getSupportedOperations() { + return SUPPORT_IMPORT; + } + + @Override + public boolean Import() { + return mMtpContext.copyAlbum(mDeviceName, mName, mJpegChildren); + } + + @Override + public boolean isLeafAlbum() { + return true; + } +} diff --git a/src/com/android/gallery3d/data/MtpDeviceSet.java b/src/com/android/gallery3d/data/MtpDeviceSet.java new file mode 100644 index 000000000..6521623d4 --- /dev/null +++ b/src/com/android/gallery3d/data/MtpDeviceSet.java @@ -0,0 +1,109 @@ +/* + * 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.data; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.util.MediaSetUtils; + +import android.mtp.MtpDeviceInfo; +import android.net.Uri; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +// MtpDeviceSet -- MtpDevice -- MtpImage +public class MtpDeviceSet extends MediaSet { + private static final String TAG = "MtpDeviceSet"; + + private GalleryApp mApplication; + private final ArrayList<MediaSet> mDeviceSet = new ArrayList<MediaSet>(); + private final ChangeNotifier mNotifier; + private final MtpContext mMtpContext; + private final String mName; + + public MtpDeviceSet(Path path, GalleryApp application, MtpContext mtpContext) { + super(path, nextVersionNumber()); + mApplication = application; + mNotifier = new ChangeNotifier(this, Uri.parse("mtp://"), application); + mMtpContext = mtpContext; + mName = application.getResources().getString(R.string.set_label_mtp_devices); + } + + private void loadDevices() { + DataManager dataManager = mApplication.getDataManager(); + // Enumerate all devices + mDeviceSet.clear(); + List<android.mtp.MtpDevice> devices = mMtpContext.getMtpClient().getDeviceList(); + Log.v(TAG, "loadDevices: " + devices + ", size=" + devices.size()); + for (android.mtp.MtpDevice mtpDevice : devices) { + int deviceId = mtpDevice.getDeviceId(); + Path childPath = mPath.getChild(deviceId); + MtpDevice device = (MtpDevice) dataManager.peekMediaObject(childPath); + if (device == null) { + device = new MtpDevice(childPath, mApplication, deviceId, mMtpContext); + } + Log.d(TAG, "add device " + device); + mDeviceSet.add(device); + } + + Collections.sort(mDeviceSet, MediaSetUtils.NAME_COMPARATOR); + for (int i = 0, n = mDeviceSet.size(); i < n; i++) { + mDeviceSet.get(i).reload(); + } + } + + public static String getDeviceName(MtpContext mtpContext, int deviceId) { + android.mtp.MtpDevice device = mtpContext.getMtpClient().getDevice(deviceId); + if (device == null) { + return ""; + } + MtpDeviceInfo info = device.getDeviceInfo(); + if (info == null) { + return ""; + } + String manufacturer = info.getManufacturer().trim(); + String model = info.getModel().trim(); + return manufacturer + " " + model; + } + + @Override + public MediaSet getSubMediaSet(int index) { + return index < mDeviceSet.size() ? mDeviceSet.get(index) : null; + } + + @Override + public int getSubMediaSetCount() { + return mDeviceSet.size(); + } + + @Override + public String getName() { + return mName; + } + + @Override + public long reload() { + if (mNotifier.isDirty()) { + mDataVersion = nextVersionNumber(); + loadDevices(); + } + return mDataVersion; + } +} diff --git a/src/com/android/gallery3d/data/MtpImage.java b/src/com/android/gallery3d/data/MtpImage.java new file mode 100644 index 000000000..4766d88f8 --- /dev/null +++ b/src/com/android/gallery3d/data/MtpImage.java @@ -0,0 +1,166 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.provider.GalleryProvider; +import com.android.gallery3d.util.ThreadPool; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.graphics.Bitmap; +import android.graphics.BitmapRegionDecoder; +import android.hardware.usb.UsbDevice; +import android.mtp.MtpObjectInfo; +import android.net.Uri; +import android.util.Log; + +import java.text.DateFormat; +import java.util.Date; + +public class MtpImage extends MediaItem { + private static final String TAG = "MtpImage"; + + private final int mDeviceId; + private int mObjectId; + private int mObjectSize; + private long mDateTaken; + private String mFileName; + private final ThreadPool mThreadPool; + private final MtpContext mMtpContext; + private final MtpObjectInfo mObjInfo; + private final int mImageWidth; + private final int mImageHeight; + + MtpImage(Path path, GalleryApp application, int deviceId, + MtpObjectInfo objInfo, MtpContext mtpContext) { + super(path, nextVersionNumber()); + mDeviceId = deviceId; + mObjInfo = objInfo; + mObjectId = objInfo.getObjectHandle(); + mObjectSize = objInfo.getCompressedSize(); + mDateTaken = objInfo.getDateCreated(); + mFileName = objInfo.getName(); + mImageWidth = objInfo.getImagePixWidth(); + mImageHeight = objInfo.getImagePixHeight(); + mThreadPool = application.getThreadPool(); + mMtpContext = mtpContext; + } + + MtpImage(Path path, GalleryApp app, int deviceId, int objectId, MtpContext mtpContext) { + this(path, app, deviceId, MtpDevice.getObjectInfo(mtpContext, deviceId, objectId), + mtpContext); + } + + @Override + public long getDateInMs() { + return mDateTaken; + } + + @Override + public Job<Bitmap> requestImage(int type) { + return new Job<Bitmap>() { + public Bitmap run(JobContext jc) { + GetThumbnailBytes job = new GetThumbnailBytes(); + byte[] thumbnail = mThreadPool.submit(job).get(); + if (thumbnail == null) { + Log.w(TAG, "decoding thumbnail failed"); + return null; + } + return DecodeUtils.requestDecode(jc, thumbnail, null); + } + }; + } + + @Override + public Job<BitmapRegionDecoder> requestLargeImage() { + return new Job<BitmapRegionDecoder>() { + public BitmapRegionDecoder run(JobContext jc) { + byte[] bytes = mMtpContext.getMtpClient().getObject( + UsbDevice.getDeviceName(mDeviceId), mObjectId, mObjectSize); + return DecodeUtils.requestCreateBitmapRegionDecoder( + jc, bytes, 0, bytes.length, false); + } + }; + } + + public byte[] getImageData() { + return mMtpContext.getMtpClient().getObject( + UsbDevice.getDeviceName(mDeviceId), mObjectId, mObjectSize); + } + + @Override + public boolean Import() { + return mMtpContext.copyFile(UsbDevice.getDeviceName(mDeviceId), mObjInfo); + } + + @Override + public int getSupportedOperations() { + return SUPPORT_FULL_IMAGE | SUPPORT_IMPORT; + } + + private class GetThumbnailBytes implements Job<byte[]> { + public byte[] run(JobContext jc) { + return mMtpContext.getMtpClient().getThumbnail( + UsbDevice.getDeviceName(mDeviceId), mObjectId); + } + } + + public void updateContent(MtpObjectInfo info) { + if (mObjectId != info.getObjectHandle() || mDateTaken != info.getDateCreated()) { + mObjectId = info.getObjectHandle(); + mDateTaken = info.getDateCreated(); + mDataVersion = nextVersionNumber(); + } + } + + @Override + public String getMimeType() { + // Currently only JPEG is supported in MTP. + return "image/jpeg"; + } + + @Override + public int getMediaType() { + return MEDIA_TYPE_IMAGE; + } + + @Override + public long getSize() { + return mObjectSize; + } + + @Override + public Uri getContentUri() { + return GalleryProvider.BASE_URI.buildUpon() + .appendEncodedPath(mPath.toString().substring(1)) + .build(); + } + + @Override + public MediaDetails getDetails() { + MediaDetails details = super.getDetails(); + DateFormat formater = DateFormat.getDateTimeInstance(); + details.addDetail(MediaDetails.INDEX_TITLE, mFileName); + details.addDetail(MediaDetails.INDEX_DATETIME, formater.format(new Date(mDateTaken))); + details.addDetail(MediaDetails.INDEX_WIDTH, mImageWidth); + details.addDetail(MediaDetails.INDEX_HEIGHT, mImageHeight); + details.addDetail(MediaDetails.INDEX_SIZE, Long.valueOf(mObjectSize)); + return details; + } + +} diff --git a/src/com/android/gallery3d/data/MtpSource.java b/src/com/android/gallery3d/data/MtpSource.java new file mode 100644 index 000000000..683a40291 --- /dev/null +++ b/src/com/android/gallery3d/data/MtpSource.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.data; + +import com.android.gallery3d.app.GalleryApp; + +class MtpSource extends MediaSource { + private static final String TAG = "MtpSource"; + + private static final int MTP_DEVICESET = 0; + private static final int MTP_DEVICE = 1; + private static final int MTP_ITEM = 2; + + GalleryApp mApplication; + PathMatcher mMatcher; + MtpContext mMtpContext; + + public MtpSource(GalleryApp application) { + super("mtp"); + mApplication = application; + mMatcher = new PathMatcher(); + mMatcher.add("/mtp", MTP_DEVICESET); + mMatcher.add("/mtp/*", MTP_DEVICE); + mMatcher.add("/mtp/item/*/*", MTP_ITEM); + mMtpContext = new MtpContext(mApplication.getAndroidContext()); + } + + @Override + public MediaObject createMediaObject(Path path) { + switch (mMatcher.match(path)) { + case MTP_DEVICESET: { + return new MtpDeviceSet(path, mApplication, mMtpContext); + } + case MTP_DEVICE: { + int deviceId = mMatcher.getIntVar(0); + return new MtpDevice(path, mApplication, deviceId, mMtpContext); + } + case MTP_ITEM: { + int deviceId = mMatcher.getIntVar(0); + int objectId = mMatcher.getIntVar(1); + return new MtpImage(path, mApplication, deviceId, objectId, mMtpContext); + } + default: + throw new RuntimeException("bad path: " + path); + } + } + + @Override + public void pause() { + mMtpContext.pause(); + } + + @Override + public void resume() { + mMtpContext.resume(); + } +} diff --git a/src/com/android/gallery3d/data/Path.java b/src/com/android/gallery3d/data/Path.java new file mode 100644 index 000000000..3de1c7c76 --- /dev/null +++ b/src/com/android/gallery3d/data/Path.java @@ -0,0 +1,237 @@ +/* + * 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.data; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.IdentityCache; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +public class Path { + private static final String TAG = "Path"; + private static Path sRoot = new Path(null, "ROOT"); + + private final Path mParent; + private final String mSegment; + private WeakReference<MediaObject> mObject; + private IdentityCache<String, Path> mChildren; + + private Path(Path parent, String segment) { + mParent = parent; + mSegment = segment; + } + + public Path getChild(String segment) { + synchronized (Path.class) { + if (mChildren == null) { + mChildren = new IdentityCache<String, Path>(); + } else { + Path p = mChildren.get(segment); + if (p != null) return p; + } + + Path p = new Path(this, segment); + mChildren.put(segment, p); + return p; + } + } + + public Path getParent() { + synchronized (Path.class) { + return mParent; + } + } + + public Path getChild(int segment) { + return getChild(String.valueOf(segment)); + } + + public Path getChild(long segment) { + return getChild(String.valueOf(segment)); + } + + public void setObject(MediaObject object) { + synchronized (Path.class) { + Utils.assertTrue(mObject == null || mObject.get() == null); + mObject = new WeakReference<MediaObject>(object); + } + } + + public MediaObject getObject() { + synchronized (Path.class) { + return (mObject == null) ? null : mObject.get(); + } + } + + @Override + public String toString() { + synchronized (Path.class) { + StringBuilder sb = new StringBuilder(); + String[] segments = split(); + for (int i = 0; i < segments.length; i++) { + sb.append("/"); + sb.append(segments[i]); + } + return sb.toString(); + } + } + + public static Path fromString(String s) { + synchronized (Path.class) { + String[] segments = split(s); + Path current = sRoot; + for (int i = 0; i < segments.length; i++) { + current = current.getChild(segments[i]); + } + return current; + } + } + + public String[] split() { + synchronized (Path.class) { + int n = 0; + for (Path p = this; p != sRoot; p = p.mParent) { + n++; + } + String[] segments = new String[n]; + int i = n - 1; + for (Path p = this; p != sRoot; p = p.mParent) { + segments[i--] = p.mSegment; + } + return segments; + } + } + + public static String[] split(String s) { + int n = s.length(); + if (n == 0) return new String[0]; + if (s.charAt(0) != '/') { + throw new RuntimeException("malformed path:" + s); + } + ArrayList<String> segments = new ArrayList<String>(); + int i = 1; + while (i < n) { + int brace = 0; + int j; + for (j = i; j < n; j++) { + char c = s.charAt(j); + if (c == '{') ++brace; + else if (c == '}') --brace; + else if (brace == 0 && c == '/') break; + } + if (brace != 0) { + throw new RuntimeException("unbalanced brace in path:" + s); + } + segments.add(s.substring(i, j)); + i = j + 1; + } + String[] result = new String[segments.size()]; + segments.toArray(result); + return result; + } + + // Splits a string to an array of strings. + // For example, "{foo,bar,baz}" -> {"foo","bar","baz"}. + public static String[] splitSequence(String s) { + int n = s.length(); + if (s.charAt(0) != '{' || s.charAt(n-1) != '}') { + throw new RuntimeException("bad sequence: " + s); + } + ArrayList<String> segments = new ArrayList<String>(); + int i = 1; + while (i < n - 1) { + int brace = 0; + int j; + for (j = i; j < n - 1; j++) { + char c = s.charAt(j); + if (c == '{') ++brace; + else if (c == '}') --brace; + else if (brace == 0 && c == ',') break; + } + if (brace != 0) { + throw new RuntimeException("unbalanced brace in path:" + s); + } + segments.add(s.substring(i, j)); + i = j + 1; + } + String[] result = new String[segments.size()]; + segments.toArray(result); + return result; + } + + public String getPrefix() { + synchronized (Path.class) { + Path current = this; + if (current == sRoot) return ""; + while (current.mParent != sRoot) { + current = current.mParent; + } + return current.mSegment; + } + } + + public String getSuffix() { + // We don't need lock because mSegment is final. + return mSegment; + } + + public String getSuffix(int level) { + // We don't need lock because mSegment and mParent are final. + Path p = this; + while (level-- != 0) { + p = p.mParent; + } + return p.mSegment; + } + + // Below are for testing/debugging only + static void clearAll() { + synchronized (Path.class) { + sRoot = new Path(null, ""); + } + } + + static void dumpAll() { + dumpAll(sRoot, "", ""); + } + + static void dumpAll(Path p, String prefix1, String prefix2) { + synchronized (Path.class) { + MediaObject obj = p.getObject(); + Log.d(TAG, prefix1 + p.mSegment + ":" + + (obj == null ? "null" : obj.getClass().getSimpleName())); + if (p.mChildren != null) { + ArrayList<String> childrenKeys = p.mChildren.keys(); + int i = 0, n = childrenKeys.size(); + for (String key : childrenKeys) { + Path child = p.mChildren.get(key); + if (child == null) { + ++i; + continue; + } + Log.d(TAG, prefix2 + "|"); + if (++i < n) { + dumpAll(child, prefix2 + "+-- ", prefix2 + "| "); + } else { + dumpAll(child, prefix2 + "+-- ", prefix2 + " "); + } + } + } + } + } +} diff --git a/src/com/android/gallery3d/data/PathMatcher.java b/src/com/android/gallery3d/data/PathMatcher.java new file mode 100644 index 000000000..9c6b840d5 --- /dev/null +++ b/src/com/android/gallery3d/data/PathMatcher.java @@ -0,0 +1,102 @@ +/* + * 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.data; + +import java.util.ArrayList; +import java.util.HashMap; + +public class PathMatcher { + public static final int NOT_FOUND = -1; + + private ArrayList<String> mVariables = new ArrayList<String>(); + private Node mRoot = new Node(); + + public PathMatcher() { + mRoot = new Node(); + } + + public void add(String pattern, int kind) { + String[] segments = Path.split(pattern); + Node current = mRoot; + for (int i = 0; i < segments.length; i++) { + current = current.addChild(segments[i]); + } + current.setKind(kind); + } + + public int match(Path path) { + String[] segments = path.split(); + mVariables.clear(); + Node current = mRoot; + for (int i = 0; i < segments.length; i++) { + Node next = current.getChild(segments[i]); + if (next == null) { + next = current.getChild("*"); + if (next != null) { + mVariables.add(segments[i]); + } else { + return NOT_FOUND; + } + } + current = next; + } + return current.getKind(); + } + + public String getVar(int index) { + return mVariables.get(index); + } + + public int getIntVar(int index) { + return Integer.parseInt(mVariables.get(index)); + } + + public long getLongVar(int index) { + return Long.parseLong(mVariables.get(index)); + } + + private static class Node { + private HashMap<String, Node> mMap; + private int mKind = NOT_FOUND; + + Node addChild(String segment) { + if (mMap == null) { + mMap = new HashMap<String, Node>(); + } else { + Node node = mMap.get(segment); + if (node != null) return node; + } + + Node n = new Node(); + mMap.put(segment, n); + return n; + } + + Node getChild(String segment) { + if (mMap == null) return null; + return mMap.get(segment); + } + + void setKind(int kind) { + mKind = kind; + } + + int getKind() { + return mKind; + } + } +} diff --git a/src/com/android/gallery3d/data/SizeClustering.java b/src/com/android/gallery3d/data/SizeClustering.java new file mode 100644 index 000000000..7e24b337b --- /dev/null +++ b/src/com/android/gallery3d/data/SizeClustering.java @@ -0,0 +1,138 @@ +/* + * 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.data; + +import com.android.gallery3d.R; + +import android.content.Context; +import android.content.res.Resources; + +import java.util.ArrayList; + +public class SizeClustering extends Clustering { + private static final String TAG = "SizeClustering"; + + private Context mContext; + private ArrayList<Path>[] mClusters; + private String[] mNames; + private long mMinSizes[]; + + private static final long MEGA_BYTES = 1024L*1024; + private static final long GIGA_BYTES = 1024L*1024*1024; + + private static final long[] SIZE_LEVELS = { + 0, + 1 * MEGA_BYTES, + 10 * MEGA_BYTES, + 100 * MEGA_BYTES, + 1 * GIGA_BYTES, + 2 * GIGA_BYTES, + 4 * GIGA_BYTES, + }; + + public SizeClustering(Context context) { + mContext = context; + } + + @Override + public void run(MediaSet baseSet) { + final ArrayList<Path>[] group = + (ArrayList<Path>[]) new ArrayList[SIZE_LEVELS.length]; + baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() { + public void consume(int index, MediaItem item) { + // Find the cluster this item belongs to. + long size = item.getSize(); + int i; + for (i = 0; i < SIZE_LEVELS.length - 1; i++) { + if (size < SIZE_LEVELS[i + 1]) { + break; + } + } + + ArrayList<Path> list = group[i]; + if (list == null) { + list = new ArrayList<Path>(); + group[i] = list; + } + list.add(item.getPath()); + } + }); + + int count = 0; + for (int i = 0; i < group.length; i++) { + if (group[i] != null) { + count++; + } + } + + mClusters = (ArrayList<Path>[]) new ArrayList[count]; + mNames = new String[count]; + mMinSizes = new long[count]; + + Resources res = mContext.getResources(); + int k = 0; + // Go through group in the reverse order, so the group with the largest + // size will show first. + for (int i = group.length - 1; i >= 0; i--) { + if (group[i] == null) continue; + + mClusters[k] = group[i]; + if (i == 0) { + mNames[k] = String.format( + res.getString(R.string.size_below), getSizeString(i + 1)); + } else if (i == group.length - 1) { + mNames[k] = String.format( + res.getString(R.string.size_above), getSizeString(i)); + } else { + String minSize = getSizeString(i); + String maxSize = getSizeString(i + 1); + mNames[k] = String.format( + res.getString(R.string.size_between), minSize, maxSize); + } + mMinSizes[k] = SIZE_LEVELS[i]; + k++; + } + } + + private String getSizeString(int index) { + long bytes = SIZE_LEVELS[index]; + if (bytes >= GIGA_BYTES) { + return (bytes / GIGA_BYTES) + "GB"; + } else { + return (bytes / MEGA_BYTES) + "MB"; + } + } + + @Override + public int getNumberOfClusters() { + return mClusters.length; + } + + @Override + public ArrayList<Path> getCluster(int index) { + return mClusters[index]; + } + + @Override + public String getClusterName(int index) { + return mNames[index]; + } + + public long getMinSize(int index) { + return mMinSizes[index]; + } +} diff --git a/src/com/android/gallery3d/data/TagClustering.java b/src/com/android/gallery3d/data/TagClustering.java new file mode 100644 index 000000000..c87305132 --- /dev/null +++ b/src/com/android/gallery3d/data/TagClustering.java @@ -0,0 +1,94 @@ +/* + * 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.data; + +import com.android.gallery3d.R; + +import android.content.Context; + +import java.util.ArrayList; +import java.util.Map; +import java.util.TreeMap; + +public class TagClustering extends Clustering { + @SuppressWarnings("unused") + private static final String TAG = "TagClustering"; + + private ArrayList<ArrayList<Path>> mClusters; + private String[] mNames; + private String mUntaggedString; + + public TagClustering(Context context) { + mUntaggedString = context.getResources().getString(R.string.untagged); + } + + @Override + public void run(MediaSet baseSet) { + final TreeMap<String, ArrayList<Path>> map = + new TreeMap<String, ArrayList<Path>>(); + final ArrayList<Path> untagged = new ArrayList<Path>(); + + baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() { + public void consume(int index, MediaItem item) { + Path path = item.getPath(); + + String[] tags = item.getTags(); + if (tags == null || tags.length == 0) { + untagged.add(path); + return; + } + for (int j = 0; j < tags.length; j++) { + String key = tags[j]; + ArrayList<Path> list = map.get(key); + if (list == null) { + list = new ArrayList<Path>(); + map.put(key, list); + } + list.add(path); + } + } + }); + + int m = map.size(); + mClusters = new ArrayList<ArrayList<Path>>(); + mNames = new String[m + ((untagged.size() > 0) ? 1 : 0)]; + int i = 0; + for (Map.Entry<String, ArrayList<Path>> entry : map.entrySet()) { + mNames[i++] = entry.getKey(); + mClusters.add(entry.getValue()); + } + if (untagged.size() > 0) { + mNames[i++] = mUntaggedString; + mClusters.add(untagged); + } + } + + @Override + public int getNumberOfClusters() { + return mClusters.size(); + } + + @Override + public ArrayList<Path> getCluster(int index) { + return mClusters.get(index); + } + + @Override + public String getClusterName(int index) { + return mNames[index]; + } +} diff --git a/src/com/android/gallery3d/data/TimeClustering.java b/src/com/android/gallery3d/data/TimeClustering.java new file mode 100644 index 000000000..1ccf14c13 --- /dev/null +++ b/src/com/android/gallery3d/data/TimeClustering.java @@ -0,0 +1,436 @@ +/* + * 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.data; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.GalleryUtils; + +import android.content.Context; +import android.text.format.DateFormat; +import android.text.format.DateUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +public class TimeClustering extends Clustering { + private static final String TAG = "TimeClustering"; + + // If 2 items are greater than 25 miles apart, they will be in different + // clusters. + private static final int GEOGRAPHIC_DISTANCE_CUTOFF_IN_MILES = 20; + + // Do not want to split based on anything under 1 min. + private static final long MIN_CLUSTER_SPLIT_TIME_IN_MS = 60000L; + + // Disregard a cluster split time of anything over 2 hours. + private static final long MAX_CLUSTER_SPLIT_TIME_IN_MS = 7200000L; + + // Try and get around 9 clusters (best-effort for the common case). + private static final int NUM_CLUSTERS_TARGETED = 9; + + // Try and merge 2 clusters if they are both smaller than min cluster size. + // The min cluster size can range from 8 to 15. + private static final int MIN_MIN_CLUSTER_SIZE = 8; + private static final int MAX_MIN_CLUSTER_SIZE = 15; + + // Try and split a cluster if it is bigger than max cluster size. + // The max cluster size can range from 20 to 50. + private static final int MIN_MAX_CLUSTER_SIZE = 20; + private static final int MAX_MAX_CLUSTER_SIZE = 50; + + // Initially put 2 items in the same cluster as long as they are within + // 3 cluster frequencies of each other. + private static int CLUSTER_SPLIT_MULTIPLIER = 3; + + // The minimum change factor in the time between items to consider a + // partition. + // Example: (Item 3 - Item 2) / (Item 2 - Item 1). + private static final int MIN_PARTITION_CHANGE_FACTOR = 2; + + // Make the cluster split time of a large cluster half that of a regular + // cluster. + private static final int PARTITION_CLUSTER_SPLIT_TIME_FACTOR = 2; + + private Context mContext; + private ArrayList<Cluster> mClusters; + private String[] mNames; + private Cluster mCurrCluster; + + private long mClusterSplitTime = + (MIN_CLUSTER_SPLIT_TIME_IN_MS + MAX_CLUSTER_SPLIT_TIME_IN_MS) / 2; + private long mLargeClusterSplitTime = + mClusterSplitTime / PARTITION_CLUSTER_SPLIT_TIME_FACTOR; + private int mMinClusterSize = (MIN_MIN_CLUSTER_SIZE + MAX_MIN_CLUSTER_SIZE) / 2; + private int mMaxClusterSize = (MIN_MAX_CLUSTER_SIZE + MAX_MAX_CLUSTER_SIZE) / 2; + + + private static final Comparator<SmallItem> sDateComparator = + new DateComparator(); + + private static class DateComparator implements Comparator<SmallItem> { + public int compare(SmallItem item1, SmallItem item2) { + return -Utils.compare(item1.dateInMs, item2.dateInMs); + } + } + + public TimeClustering(Context context) { + mContext = context; + mClusters = new ArrayList<Cluster>(); + mCurrCluster = new Cluster(); + } + + @Override + public void run(MediaSet baseSet) { + final int total = baseSet.getTotalMediaItemCount(); + final SmallItem[] buf = new SmallItem[total]; + final double[] latLng = new double[2]; + + baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() { + public void consume(int index, MediaItem item) { + if (index < 0 || index >= total) return; + SmallItem s = new SmallItem(); + s.path = item.getPath(); + s.dateInMs = item.getDateInMs(); + item.getLatLong(latLng); + s.lat = latLng[0]; + s.lng = latLng[1]; + buf[index] = s; + } + }); + + ArrayList<SmallItem> items = new ArrayList<SmallItem>(total); + for (int i = 0; i < total; i++) { + if (buf[i] != null) { + items.add(buf[i]); + } + } + + Collections.sort(items, sDateComparator); + + int n = items.size(); + long minTime = 0; + long maxTime = 0; + for (int i = 0; i < n; i++) { + long t = items.get(i).dateInMs; + if (t == 0) continue; + if (minTime == 0) { + minTime = maxTime = t; + } else { + minTime = Math.min(minTime, t); + maxTime = Math.max(maxTime, t); + } + } + + setTimeRange(maxTime - minTime, n); + + for (int i = 0; i < n; i++) { + compute(items.get(i)); + } + + compute(null); + + int m = mClusters.size(); + mNames = new String[m]; + for (int i = 0; i < m; i++) { + mNames[i] = mClusters.get(i).generateCaption(mContext); + } + } + + @Override + public int getNumberOfClusters() { + return mClusters.size(); + } + + @Override + public ArrayList<Path> getCluster(int index) { + ArrayList<SmallItem> items = mClusters.get(index).getItems(); + ArrayList<Path> result = new ArrayList<Path>(items.size()); + for (int i = 0, n = items.size(); i < n; i++) { + result.add(items.get(i).path); + } + return result; + } + + @Override + public String getClusterName(int index) { + return mNames[index]; + } + + private void setTimeRange(long timeRange, int numItems) { + if (numItems != 0) { + int meanItemsPerCluster = numItems / NUM_CLUSTERS_TARGETED; + // Heuristic to get min and max cluster size - half and double the + // desired items per cluster. + mMinClusterSize = meanItemsPerCluster / 2; + mMaxClusterSize = meanItemsPerCluster * 2; + mClusterSplitTime = timeRange / numItems * CLUSTER_SPLIT_MULTIPLIER; + } + mClusterSplitTime = Utils.clamp(mClusterSplitTime, MIN_CLUSTER_SPLIT_TIME_IN_MS, MAX_CLUSTER_SPLIT_TIME_IN_MS); + mLargeClusterSplitTime = mClusterSplitTime / PARTITION_CLUSTER_SPLIT_TIME_FACTOR; + mMinClusterSize = Utils.clamp(mMinClusterSize, MIN_MIN_CLUSTER_SIZE, MAX_MIN_CLUSTER_SIZE); + mMaxClusterSize = Utils.clamp(mMaxClusterSize, MIN_MAX_CLUSTER_SIZE, MAX_MAX_CLUSTER_SIZE); + } + + private void compute(SmallItem currentItem) { + if (currentItem != null) { + int numClusters = mClusters.size(); + int numCurrClusterItems = mCurrCluster.size(); + boolean geographicallySeparateItem = false; + boolean itemAddedToCurrentCluster = false; + + // Determine if this item should go in the current cluster or be the + // start of a new cluster. + if (numCurrClusterItems == 0) { + mCurrCluster.addItem(currentItem); + } else { + SmallItem prevItem = mCurrCluster.getLastItem(); + if (isGeographicallySeparated(prevItem, currentItem)) { + mClusters.add(mCurrCluster); + geographicallySeparateItem = true; + } else if (numCurrClusterItems > mMaxClusterSize) { + splitAndAddCurrentCluster(); + } else if (timeDistance(prevItem, currentItem) < mClusterSplitTime) { + mCurrCluster.addItem(currentItem); + itemAddedToCurrentCluster = true; + } else if (numClusters > 0 && numCurrClusterItems < mMinClusterSize + && !mCurrCluster.mGeographicallySeparatedFromPrevCluster) { + mergeAndAddCurrentCluster(); + } else { + mClusters.add(mCurrCluster); + } + + // Creating a new cluster and adding the current item to it. + if (!itemAddedToCurrentCluster) { + mCurrCluster = new Cluster(); + if (geographicallySeparateItem) { + mCurrCluster.mGeographicallySeparatedFromPrevCluster = true; + } + mCurrCluster.addItem(currentItem); + } + } + } else { + if (mCurrCluster.size() > 0) { + int numClusters = mClusters.size(); + int numCurrClusterItems = mCurrCluster.size(); + + // The last cluster may potentially be too big or too small. + if (numCurrClusterItems > mMaxClusterSize) { + splitAndAddCurrentCluster(); + } else if (numClusters > 0 && numCurrClusterItems < mMinClusterSize + && !mCurrCluster.mGeographicallySeparatedFromPrevCluster) { + mergeAndAddCurrentCluster(); + } else { + mClusters.add(mCurrCluster); + } + mCurrCluster = new Cluster(); + } + } + } + + private void splitAndAddCurrentCluster() { + ArrayList<SmallItem> currClusterItems = mCurrCluster.getItems(); + int numCurrClusterItems = mCurrCluster.size(); + int secondPartitionStartIndex = getPartitionIndexForCurrentCluster(); + if (secondPartitionStartIndex != -1) { + Cluster partitionedCluster = new Cluster(); + for (int j = 0; j < secondPartitionStartIndex; j++) { + partitionedCluster.addItem(currClusterItems.get(j)); + } + mClusters.add(partitionedCluster); + partitionedCluster = new Cluster(); + for (int j = secondPartitionStartIndex; j < numCurrClusterItems; j++) { + partitionedCluster.addItem(currClusterItems.get(j)); + } + mClusters.add(partitionedCluster); + } else { + mClusters.add(mCurrCluster); + } + } + + private int getPartitionIndexForCurrentCluster() { + int partitionIndex = -1; + float largestChange = MIN_PARTITION_CHANGE_FACTOR; + ArrayList<SmallItem> currClusterItems = mCurrCluster.getItems(); + int numCurrClusterItems = mCurrCluster.size(); + int minClusterSize = mMinClusterSize; + + // Could be slightly more efficient here but this code seems cleaner. + if (numCurrClusterItems > minClusterSize + 1) { + for (int i = minClusterSize; i < numCurrClusterItems - minClusterSize; i++) { + SmallItem prevItem = currClusterItems.get(i - 1); + SmallItem currItem = currClusterItems.get(i); + SmallItem nextItem = currClusterItems.get(i + 1); + + long timeNext = nextItem.dateInMs; + long timeCurr = currItem.dateInMs; + long timePrev = prevItem.dateInMs; + + if (timeNext == 0 || timeCurr == 0 || timePrev == 0) continue; + + long diff1 = Math.abs(timeNext - timeCurr); + long diff2 = Math.abs(timeCurr - timePrev); + + float change = Math.max(diff1 / (diff2 + 0.01f), diff2 / (diff1 + 0.01f)); + if (change > largestChange) { + if (timeDistance(currItem, prevItem) > mLargeClusterSplitTime) { + partitionIndex = i; + largestChange = change; + } else if (timeDistance(nextItem, currItem) > mLargeClusterSplitTime) { + partitionIndex = i + 1; + largestChange = change; + } + } + } + } + return partitionIndex; + } + + private void mergeAndAddCurrentCluster() { + int numClusters = mClusters.size(); + Cluster prevCluster = mClusters.get(numClusters - 1); + ArrayList<SmallItem> currClusterItems = mCurrCluster.getItems(); + int numCurrClusterItems = mCurrCluster.size(); + if (prevCluster.size() < mMinClusterSize) { + for (int i = 0; i < numCurrClusterItems; i++) { + prevCluster.addItem(currClusterItems.get(i)); + } + mClusters.set(numClusters - 1, prevCluster); + } else { + mClusters.add(mCurrCluster); + } + } + + // Returns true if a, b are sufficiently geographically separated. + private static boolean isGeographicallySeparated(SmallItem itemA, SmallItem itemB) { + if (!GalleryUtils.isValidLocation(itemA.lat, itemA.lng) + || !GalleryUtils.isValidLocation(itemB.lat, itemB.lng)) { + return false; + } + + double distance = GalleryUtils.fastDistanceMeters( + Math.toRadians(itemA.lat), + Math.toRadians(itemA.lng), + Math.toRadians(itemB.lat), + Math.toRadians(itemB.lng)); + return (GalleryUtils.toMile(distance) > GEOGRAPHIC_DISTANCE_CUTOFF_IN_MILES); + } + + // Returns the time interval between the two items in milliseconds. + private static long timeDistance(SmallItem a, SmallItem b) { + return Math.abs(a.dateInMs - b.dateInMs); + } +} + +class SmallItem { + Path path; + long dateInMs; + double lat, lng; +} + +class Cluster { + @SuppressWarnings("unused") + private static final String TAG = "Cluster"; + private static final String MMDDYY_FORMAT = "MMddyy"; + + // This is for TimeClustering only. + public boolean mGeographicallySeparatedFromPrevCluster = false; + + private ArrayList<SmallItem> mItems = new ArrayList<SmallItem>(); + + public Cluster() { + } + + public void addItem(SmallItem item) { + mItems.add(item); + } + + public int size() { + return mItems.size(); + } + + public SmallItem getLastItem() { + int n = mItems.size(); + return (n == 0) ? null : mItems.get(n - 1); + } + + public ArrayList<SmallItem> getItems() { + return mItems; + } + + public String generateCaption(Context context) { + int n = mItems.size(); + long minTimestamp = 0; + long maxTimestamp = 0; + + for (int i = 0; i < n; i++) { + long t = mItems.get(i).dateInMs; + if (t == 0) continue; + if (minTimestamp == 0) { + minTimestamp = maxTimestamp = t; + } else { + minTimestamp = Math.min(minTimestamp, t); + maxTimestamp = Math.max(maxTimestamp, t); + } + } + if (minTimestamp == 0) return ""; + + String caption; + String minDay = DateFormat.format(MMDDYY_FORMAT, minTimestamp) + .toString(); + String maxDay = DateFormat.format(MMDDYY_FORMAT, maxTimestamp) + .toString(); + + if (minDay.substring(4).equals(maxDay.substring(4))) { + // The items are from the same year - show at least as + // much granularity as abbrev_all allows. + caption = DateUtils.formatDateRange(context, minTimestamp, + maxTimestamp, DateUtils.FORMAT_ABBREV_ALL); + + // Get a more granular date range string if the min and + // max timestamp are on the same day and from the + // current year. + if (minDay.equals(maxDay)) { + int flags = DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_DATE; + // Contains the year only if the date does not + // correspond to the current year. + String dateRangeWithOptionalYear = DateUtils.formatDateTime( + context, minTimestamp, flags); + String dateRangeWithYear = DateUtils.formatDateTime( + context, minTimestamp, flags | DateUtils.FORMAT_SHOW_YEAR); + if (!dateRangeWithOptionalYear.equals(dateRangeWithYear)) { + // This means both dates are from the same year + // - show the time. + // Not enough room to display the time range. + // Pick the mid-point. + long midTimestamp = (minTimestamp + maxTimestamp) / 2; + caption = DateUtils.formatDateRange(context, midTimestamp, + midTimestamp, DateUtils.FORMAT_SHOW_TIME | flags); + } + } + } else { + // The items are not from the same year - only show + // month and year. + int flags = DateUtils.FORMAT_NO_MONTH_DAY + | DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_DATE; + caption = DateUtils.formatDateRange(context, minTimestamp, + maxTimestamp, flags); + } + + return caption; + } +} diff --git a/src/com/android/gallery3d/data/UriImage.java b/src/com/android/gallery3d/data/UriImage.java new file mode 100644 index 000000000..3a7ed7c3f --- /dev/null +++ b/src/com/android/gallery3d/data/UriImage.java @@ -0,0 +1,266 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.ThreadPool.CancelListener; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.content.ContentResolver; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory.Options; +import android.graphics.BitmapRegionDecoder; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.webkit.MimeTypeMap; + +import java.io.FileNotFoundException; +import java.net.URI; +import java.net.URL; + +public class UriImage extends MediaItem { + private static final String TAG = "UriImage"; + + private static final int STATE_INIT = 0; + private static final int STATE_DOWNLOADING = 1; + private static final int STATE_DOWNLOADED = 2; + private static final int STATE_ERROR = -1; + + private final Uri mUri; + private final String mContentType; + + private DownloadCache.Entry mCacheEntry; + private ParcelFileDescriptor mFileDescriptor; + private int mState = STATE_INIT; + private int mWidth; + private int mHeight; + + private GalleryApp mApplication; + + public UriImage(GalleryApp application, Path path, Uri uri) { + super(path, nextVersionNumber()); + mUri = uri; + mApplication = Utils.checkNotNull(application); + mContentType = getMimeType(uri); + } + + private String getMimeType(Uri uri) { + if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { + String extension = + MimeTypeMap.getFileExtensionFromUrl(uri.toString()); + String type = MimeTypeMap.getSingleton() + .getMimeTypeFromExtension(extension); + if (type != null) return type; + } + return mApplication.getContentResolver().getType(uri); + } + + @Override + public Job<Bitmap> requestImage(int type) { + return new BitmapJob(type); + } + + @Override + public Job<BitmapRegionDecoder> requestLargeImage() { + return new RegionDecoderJob(); + } + + private void openFileOrDownloadTempFile(JobContext jc) { + int state = openOrDownloadInner(jc); + synchronized (this) { + mState = state; + if (mState != STATE_DOWNLOADED) { + if (mFileDescriptor != null) { + Utils.closeSilently(mFileDescriptor); + mFileDescriptor = null; + } + } + notifyAll(); + } + } + + private int openOrDownloadInner(JobContext jc) { + String scheme = mUri.getScheme(); + if (ContentResolver.SCHEME_CONTENT.equals(scheme) + || ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme) + || ContentResolver.SCHEME_FILE.equals(scheme)) { + try { + mFileDescriptor = mApplication.getContentResolver() + .openFileDescriptor(mUri, "r"); + if (jc.isCancelled()) return STATE_INIT; + return STATE_DOWNLOADED; + } catch (FileNotFoundException e) { + Log.w(TAG, "fail to open: " + mUri, e); + return STATE_ERROR; + } + } else { + try { + URL url = new URI(mUri.toString()).toURL(); + mCacheEntry = mApplication.getDownloadCache().download(jc, url); + if (jc.isCancelled()) return STATE_INIT; + if (mCacheEntry == null) { + Log.w(TAG, "download failed " + url); + return STATE_ERROR; + } + mFileDescriptor = ParcelFileDescriptor.open( + mCacheEntry.cacheFile, ParcelFileDescriptor.MODE_READ_ONLY); + return STATE_DOWNLOADED; + } catch (Throwable t) { + Log.w(TAG, "download error", t); + return STATE_ERROR; + } + } + } + + private boolean prepareInputFile(JobContext jc) { + jc.setCancelListener(new CancelListener() { + public void onCancel() { + synchronized (this) { + notifyAll(); + } + } + }); + + while (true) { + synchronized (this) { + if (jc.isCancelled()) return false; + if (mState == STATE_INIT) { + mState = STATE_DOWNLOADING; + // Then leave the synchronized block and continue. + } else if (mState == STATE_ERROR) { + return false; + } else if (mState == STATE_DOWNLOADED) { + return true; + } else /* if (mState == STATE_DOWNLOADING) */ { + try { + wait(); + } catch (InterruptedException ex) { + // ignored. + } + continue; + } + } + // This is only reached for STATE_INIT->STATE_DOWNLOADING + openFileOrDownloadTempFile(jc); + } + } + + private class RegionDecoderJob implements Job<BitmapRegionDecoder> { + public BitmapRegionDecoder run(JobContext jc) { + if (!prepareInputFile(jc)) return null; + BitmapRegionDecoder decoder = DecodeUtils.requestCreateBitmapRegionDecoder( + jc, mFileDescriptor.getFileDescriptor(), false); + mWidth = decoder.getWidth(); + mHeight = decoder.getHeight(); + return decoder; + } + } + + private class BitmapJob implements Job<Bitmap> { + private int mType; + + protected BitmapJob(int type) { + mType = type; + } + + public Bitmap run(JobContext jc) { + if (!prepareInputFile(jc)) return null; + int targetSize = LocalImage.getTargetSize(mType); + Options options = new Options(); + options.inPreferredConfig = Config.ARGB_8888; + Bitmap bitmap = DecodeUtils.requestDecode(jc, + mFileDescriptor.getFileDescriptor(), options, targetSize); + if (jc.isCancelled() || bitmap == null) { + return null; + } + + if (mType == MediaItem.TYPE_MICROTHUMBNAIL) { + bitmap = BitmapUtils.resizeDownAndCropCenter(bitmap, + targetSize, true); + } else { + bitmap = BitmapUtils.resizeDownBySideLength(bitmap, + targetSize, true); + } + + return bitmap; + } + } + + @Override + public int getSupportedOperations() { + int supported = SUPPORT_EDIT | SUPPORT_SETAS; + if (isSharable()) supported |= SUPPORT_SHARE; + if (BitmapUtils.isSupportedByRegionDecoder(mContentType)) { + supported |= SUPPORT_FULL_IMAGE; + } + return supported; + } + + private boolean isSharable() { + // We cannot grant read permission to the receiver since we put + // the data URI in EXTRA_STREAM instead of the data part of an intent + // And there are issues in MediaUploader and Bluetooth file sender to + // share a general image data. So, we only share for local file. + return ContentResolver.SCHEME_FILE.equals(mUri.getScheme()); + } + + @Override + public int getMediaType() { + return MEDIA_TYPE_IMAGE; + } + + @Override + public Uri getContentUri() { + return mUri; + } + + @Override + public MediaDetails getDetails() { + MediaDetails details = super.getDetails(); + if (mWidth != 0 && mHeight != 0) { + details.addDetail(MediaDetails.INDEX_WIDTH, mWidth); + details.addDetail(MediaDetails.INDEX_HEIGHT, mHeight); + } + details.addDetail(MediaDetails.INDEX_MIMETYPE, mContentType); + if (ContentResolver.SCHEME_FILE.equals(mUri.getScheme())) { + String filePath = mUri.getPath(); + details.addDetail(MediaDetails.INDEX_PATH, filePath); + MediaDetails.extractExifInfo(details, filePath); + } + return details; + } + + @Override + public String getMimeType() { + return mContentType; + } + + @Override + protected void finalize() throws Throwable { + try { + if (mFileDescriptor != null) { + Utils.closeSilently(mFileDescriptor); + } + } finally { + super.finalize(); + } + } +} diff --git a/src/com/android/gallery3d/data/UriSource.java b/src/com/android/gallery3d/data/UriSource.java new file mode 100644 index 000000000..ac62b93a7 --- /dev/null +++ b/src/com/android/gallery3d/data/UriSource.java @@ -0,0 +1,58 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; + +import android.net.Uri; + +import java.net.URLDecoder; +import java.net.URLEncoder; + +class UriSource extends MediaSource { + @SuppressWarnings("unused") + private static final String TAG = "UriSource"; + + private GalleryApp mApplication; + + public UriSource(GalleryApp context) { + super("uri"); + mApplication = context; + } + + @Override + public MediaObject createMediaObject(Path path) { + String segment[] = path.split(); + if (segment.length != 2) { + throw new RuntimeException("bad path: " + path); + } + + String decoded = URLDecoder.decode(segment[1]); + return new UriImage(mApplication, path, Uri.parse(decoded)); + } + + @Override + public Path findPathByUri(Uri uri) { + String type = mApplication.getContentResolver().getType(uri); + // Assume the type is image if the type cannot be resolved + // This could happen for "http" URI. + if (type == null || type.startsWith("image/")) { + return Path.fromString("/uri/" + URLEncoder.encode(uri.toString())); + } + return null; + } +} diff --git a/src/com/android/gallery3d/provider/GalleryProvider.java b/src/com/android/gallery3d/provider/GalleryProvider.java new file mode 100644 index 000000000..f5f0f1b3c --- /dev/null +++ b/src/com/android/gallery3d/provider/GalleryProvider.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2009 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.provider; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.DownloadCache; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MtpImage; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.picasasource.PicasaSource; +import com.android.gallery3d.util.GalleryUtils; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.Binder; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.provider.MediaStore.Images.ImageColumns; +import android.util.Log; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; + +public class GalleryProvider extends ContentProvider { + private static final String TAG = "GalleryProvider"; + + public static final String AUTHORITY = "com.android.gallery3d.provider"; + public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY); + + private DataManager mDataManager; + private DownloadCache mDownloadCache; + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + // TODO: consider concurrent access + @Override + public String getType(Uri uri) { + long token = Binder.clearCallingIdentity(); + try { + Path path = Path.fromString(uri.getPath()); + MediaItem item = (MediaItem) mDataManager.getMediaObject(path); + return item != null ? item.getMimeType() : null; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean onCreate() { + GalleryApp app = (GalleryApp) getContext().getApplicationContext(); + mDataManager = app.getDataManager(); + return true; + } + + private DownloadCache getDownloadCache() { + if (mDownloadCache == null) { + GalleryApp app = (GalleryApp) getContext().getApplicationContext(); + mDownloadCache = app.getDownloadCache(); + } + return mDownloadCache; + } + + // TODO: consider concurrent access + @Override + public Cursor query(Uri uri, String[] projection, + String selection, String[] selectionArgs, String sortOrder) { + long token = Binder.clearCallingIdentity(); + try { + Path path = Path.fromString(uri.getPath()); + MediaObject object = mDataManager.getMediaObject(path); + if (object == null) { + Log.w(TAG, "cannot find: " + uri); + return null; + } + if (PicasaSource.isPicasaImage(object)) { + return queryPicasaItem(object, + projection, selection, selectionArgs, sortOrder); + } else if (object instanceof MtpImage) { + return queryMtpItem((MtpImage) object, + projection, selection, selectionArgs, sortOrder); + } else { + return null; + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private Cursor queryMtpItem(MtpImage image, String[] projection, + String selection, String[] selectionArgs, String sortOrder) { + Object[] columnValues = new Object[projection.length]; + for (int i = 0, n = projection.length; i < n; ++i) { + String column = projection[i]; + if (ImageColumns.DISPLAY_NAME.equals(column)) { + columnValues[i] = image.getName(); + } else if (ImageColumns.SIZE.equals(column)){ + columnValues[i] = image.getSize(); + } else if (ImageColumns.MIME_TYPE.equals(column)) { + columnValues[i] = image.getMimeType(); + } else if (ImageColumns.DATE_TAKEN.equals(column)) { + columnValues[i] = image.getDateInMs(); + } else { + Log.w(TAG, "unsupported column: " + column); + } + } + MatrixCursor cursor = new MatrixCursor(projection); + cursor.addRow(columnValues); + return cursor; + } + + private Cursor queryPicasaItem(MediaObject image, String[] projection, + String selection, String[] selectionArgs, String sortOrder) { + Object[] columnValues = new Object[projection.length]; + double latitude = PicasaSource.getLatitude(image); + double longitude = PicasaSource.getLongitude(image); + boolean isValidLatlong = GalleryUtils.isValidLocation(latitude, longitude); + + for (int i = 0, n = projection.length; i < n; ++i) { + String column = projection[i]; + if (ImageColumns.DISPLAY_NAME.equals(column)) { + columnValues[i] = PicasaSource.getImageTitle(image); + } else if (ImageColumns.SIZE.equals(column)){ + columnValues[i] = PicasaSource.getImageSize(image); + } else if (ImageColumns.MIME_TYPE.equals(column)) { + columnValues[i] = PicasaSource.getContentType(image); + } else if (ImageColumns.DATE_TAKEN.equals(column)) { + columnValues[i] = PicasaSource.getDateTaken(image); + } else if (ImageColumns.LATITUDE.equals(column)) { + columnValues[i] = isValidLatlong ? latitude : null; + } else if (ImageColumns.LONGITUDE.equals(column)) { + columnValues[i] = isValidLatlong ? longitude : null; + } else if (ImageColumns.ORIENTATION.equals(column)) { + columnValues[i] = PicasaSource.getRotation(image); + } else { + Log.w(TAG, "unsupported column: " + column); + } + } + MatrixCursor cursor = new MatrixCursor(projection); + cursor.addRow(columnValues); + return cursor; + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) + throws FileNotFoundException { + long token = Binder.clearCallingIdentity(); + try { + if (mode.contains("w")) { + throw new FileNotFoundException("cannot open file for write"); + } + Path path = Path.fromString(uri.getPath()); + MediaObject object = mDataManager.getMediaObject(path); + if (object == null) { + throw new FileNotFoundException(uri.toString()); + } + if (PicasaSource.isPicasaImage(object)) { + return PicasaSource.openFile(getContext(), object, mode); + } else if (object instanceof MtpImage) { + return openPipeHelper(uri, null, null, null, + new MtpPipeDataWriter((MtpImage) object)); + } else { + throw new FileNotFoundException("unspported type: " + object); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + private final class MtpPipeDataWriter implements PipeDataWriter<Object> { + private final MtpImage mImage; + + private MtpPipeDataWriter(MtpImage image) { + mImage = image; + } + + @Override + public void writeDataToPipe(ParcelFileDescriptor output, + Uri uri, String mimeType, Bundle opts, Object args) { + OutputStream os = null; + try { + os = new ParcelFileDescriptor.AutoCloseOutputStream(output); + os.write(mImage.getImageData()); + } catch (IOException e) { + Log.w(TAG, "fail to download: " + uri, e); + } finally { + Utils.closeSilently(os); + } + } + } +} diff --git a/src/com/android/gallery3d/ui/AbstractDisplayItem.java b/src/com/android/gallery3d/ui/AbstractDisplayItem.java new file mode 100644 index 000000000..aad3919b5 --- /dev/null +++ b/src/com/android/gallery3d/ui/AbstractDisplayItem.java @@ -0,0 +1,114 @@ +/* + * 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.ui; + +import com.android.gallery3d.data.MediaItem; + +import android.graphics.Bitmap; + +public abstract class AbstractDisplayItem extends DisplayItem { + + private static final String TAG = "AbstractDisplayItem"; + + private static final int STATE_INVALID = 0x01; + private static final int STATE_VALID = 0x02; + private static final int STATE_UPDATING = 0x04; + private static final int STATE_CANCELING = 0x08; + private static final int STATE_ERROR = 0x10; + + private int mState = STATE_INVALID; + private boolean mImageRequested = false; + private boolean mRecycling = false; + private Bitmap mBitmap; + + protected final MediaItem mMediaItem; + private int mRotation; + + public AbstractDisplayItem(MediaItem item) { + mMediaItem = item; + if (item == null) mState = STATE_ERROR; + if (item != null) mRotation = mMediaItem.getRotation(); + } + + protected void updateImage(Bitmap bitmap, boolean isCancelled) { + if (mRecycling) { + return; + } + + if (isCancelled && bitmap == null) { + mState = STATE_INVALID; + if (mImageRequested) { + // request image again. + requestImage(); + } + return; + } + + mBitmap = bitmap; + mState = bitmap == null ? STATE_ERROR : STATE_VALID ; + onBitmapAvailable(mBitmap); + } + + @Override + public int getRotation() { + return mRotation; + } + + @Override + public long getIdentity() { + return mMediaItem != null + ? System.identityHashCode(mMediaItem.getPath()) + : System.identityHashCode(this); + } + + public void requestImage() { + mImageRequested = true; + if (mState == STATE_INVALID) { + mState = STATE_UPDATING; + startLoadBitmap(); + } + } + + public void cancelImageRequest() { + mImageRequested = false; + if (mState == STATE_UPDATING) { + mState = STATE_CANCELING; + cancelLoadBitmap(); + } + } + + private boolean inState(int states) { + return (mState & states) != 0; + } + + public void recycle() { + if (!inState(STATE_UPDATING | STATE_CANCELING)) { + if (mBitmap != null) mBitmap = null; + } else { + mRecycling = true; + cancelImageRequest(); + } + } + + public boolean isRequestInProgress() { + return mImageRequested && inState(STATE_UPDATING | STATE_CANCELING); + } + + abstract protected void startLoadBitmap(); + abstract protected void cancelLoadBitmap(); + abstract protected void onBitmapAvailable(Bitmap bitmap); +} diff --git a/src/com/android/gallery3d/ui/ActionModeHandler.java b/src/com/android/gallery3d/ui/ActionModeHandler.java new file mode 100644 index 000000000..6c81a3f6a --- /dev/null +++ b/src/com/android/gallery3d/ui/ActionModeHandler.java @@ -0,0 +1,246 @@ +/* + * 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.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryActionBar; +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.CustomMenu.DropDownMenu; +import com.android.gallery3d.ui.MenuExecutor.ProgressListener; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Handler; +import android.view.ActionMode; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.PopupMenu.OnMenuItemClickListener; +import android.widget.ShareActionProvider; + +import java.util.ArrayList; + +public class ActionModeHandler implements ActionMode.Callback { + private static final String TAG = "ActionModeHandler"; + private static final int SUPPORT_MULTIPLE_MASK = MediaObject.SUPPORT_DELETE + | MediaObject.SUPPORT_ROTATE | MediaObject.SUPPORT_SHARE + | MediaObject.SUPPORT_CACHE | MediaObject.SUPPORT_IMPORT; + + public interface ActionModeListener { + public boolean onActionItemClicked(MenuItem item); + } + + private final GalleryActivity mActivity; + private final MenuExecutor mMenuExecutor; + private final SelectionManager mSelectionManager; + private Menu mMenu; + private DropDownMenu mSelectionMenu; + private ActionModeListener mListener; + private Future<?> mMenuTask; + private Handler mMainHandler; + private ShareActionProvider mShareActionProvider; + + public ActionModeHandler( + GalleryActivity activity, SelectionManager selectionManager) { + mActivity = Utils.checkNotNull(activity); + mSelectionManager = Utils.checkNotNull(selectionManager); + mMenuExecutor = new MenuExecutor(activity, selectionManager); + mMainHandler = new Handler(activity.getMainLooper()); + } + + public ActionMode startActionMode() { + Activity a = (Activity) mActivity; + final ActionMode actionMode = a.startActionMode(this); + CustomMenu customMenu = new CustomMenu(a); + View customView = LayoutInflater.from(a).inflate( + R.layout.action_mode, null); + actionMode.setCustomView(customView); + mSelectionMenu = customMenu.addDropDownMenu( + (Button) customView.findViewById(R.id.selection_menu), + R.menu.selection); + customMenu.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + return onActionItemClicked(actionMode, item); + } + }); + return actionMode; + } + + public void setTitle(String title) { + mSelectionMenu.setTitle(title); + } + + public void setActionModeListener(ActionModeListener listener) { + mListener = listener; + } + + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + boolean result; + if (mListener != null) { + result = mListener.onActionItemClicked(item); + if (result) { + mSelectionManager.leaveSelectionMode(); + return result; + } + } + ProgressListener listener = null; + if (item.getItemId() == R.id.action_import) { + listener = new ImportCompleteListener(mActivity); + } + result = mMenuExecutor.onMenuClicked(item, listener); + if (item.getItemId() == R.id.action_select_all) { + updateSupportedOperation(); + + // For clients who call SelectionManager.selectAll() directly, we need to ensure the + // menu status is consistent with selection manager. + item = mSelectionMenu.findItem(R.id.action_select_all); + if (item != null) { + if (mSelectionManager.inSelectAllMode()) { + item.setChecked(true); + item.setTitle(R.string.deselect_all); + } else { + item.setChecked(false); + item.setTitle(R.string.select_all); + } + } + } + return result; + } + + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + MenuInflater inflater = mode.getMenuInflater(); + inflater.inflate(R.menu.operation, menu); + + mShareActionProvider = GalleryActionBar.initializeShareActionProvider(menu); + + mMenu = menu; + return true; + } + + public void onDestroyActionMode(ActionMode mode) { + mSelectionManager.leaveSelectionMode(); + } + + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return true; + } + + private void updateMenuOptionsAndSharingIntent(JobContext jc) { + ArrayList<Path> paths = mSelectionManager.getSelected(true); + if (paths.size() == 0) return; + + int operation = MediaObject.SUPPORT_ALL; + DataManager manager = mActivity.getDataManager(); + final ArrayList<Uri> uris = new ArrayList<Uri>(); + int type = 0; + for (Path path : paths) { + if (jc.isCancelled()) return; + int support = manager.getSupportedOperations(path); + type |= manager.getMediaType(path); + operation &= support; + if ((support & MediaObject.SUPPORT_SHARE) != 0) { + uris.add(manager.getContentUri(path)); + } + } + final Intent intent = new Intent(); + final String mimeType = MenuExecutor.getMimeType(type); + + if (paths.size() == 1) { + if (!GalleryUtils.isEditorAvailable((Context) mActivity, mimeType)) { + operation &= ~MediaObject.SUPPORT_EDIT; + } + } else { + operation &= SUPPORT_MULTIPLE_MASK; + } + + + Log.v(TAG, "Sharing intent MIME type=" + mimeType + ", uri size = "+ uris.size()); + if (uris.size() > 1) { + intent.setAction(Intent.ACTION_SEND_MULTIPLE).setType(mimeType); + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); + } else { + intent.setAction(Intent.ACTION_SEND).setType(mimeType); + intent.putExtra(Intent.EXTRA_STREAM, uris.get(0)); + } + intent.setType(mimeType); + + final int supportedOperation = operation; + + mMainHandler.post(new Runnable() { + @Override + public void run() { + mMenuTask = null; + MenuExecutor.updateMenuOperation(mMenu, supportedOperation); + + if (mShareActionProvider != null) { + Log.v(TAG, "Sharing intent is ready: action = " + intent.getAction()); + mShareActionProvider.setShareIntent(intent); + } + } + }); + } + + public void updateSupportedOperation(Path path, boolean selected) { + // TODO: We need to improve the performance + updateSupportedOperation(); + } + + public void updateSupportedOperation() { + if (mMenuTask != null) { + mMenuTask.cancel(); + } + + // Disable share action until share intent is in good shape + if (mShareActionProvider != null) { + Log.v(TAG, "Disable sharing until intent is ready"); + mShareActionProvider.setShareIntent(null); + } + + // Generate sharing intent and update supported operations in the background + mMenuTask = mActivity.getThreadPool().submit(new Job<Void>() { + public Void run(JobContext jc) { + updateMenuOptionsAndSharingIntent(jc); + return null; + } + }); + } + + public void pause() { + if (mMenuTask != null) { + mMenuTask.cancel(); + mMenuTask = null; + } + mMenuExecutor.pause(); + } + + public void resume() { + updateSupportedOperation(); + } +} diff --git a/src/com/android/gallery3d/ui/AdaptiveBackground.java b/src/com/android/gallery3d/ui/AdaptiveBackground.java new file mode 100644 index 000000000..42cb2ccdb --- /dev/null +++ b/src/com/android/gallery3d/ui/AdaptiveBackground.java @@ -0,0 +1,128 @@ +/* + * 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.ui; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.LightingColorFilter; +import android.graphics.Paint; + +import com.android.gallery3d.anim.FloatAnimation; + +public class AdaptiveBackground extends GLView { + + private static final int BACKGROUND_WIDTH = 128; + private static final int BACKGROUND_HEIGHT = 64; + private static final int FILTERED_COLOR = 0xffaaaaaa; + private static final int ANIMATION_DURATION = 500; + + private BasicTexture mOldBackground; + private BasicTexture mBackground; + + private final Paint mPaint; + private Bitmap mPendingBitmap; + private final FloatAnimation mAnimation = + new FloatAnimation(0, 1, ANIMATION_DURATION); + + public AdaptiveBackground() { + Paint paint = new Paint(); + paint.setFilterBitmap(true); + paint.setColorFilter(new LightingColorFilter(FILTERED_COLOR, 0)); + mPaint = paint; + } + + public Bitmap getAdaptiveBitmap(Bitmap bitmap) { + Bitmap target = Bitmap.createBitmap( + BACKGROUND_WIDTH, BACKGROUND_HEIGHT, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(target); + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + int left = 0; + int top = 0; + if (width * BACKGROUND_HEIGHT > height * BACKGROUND_WIDTH) { + float scale = (float) BACKGROUND_HEIGHT / height; + canvas.scale(scale, scale); + left = (BACKGROUND_WIDTH - (int) (width * scale + 0.5)) / 2; + } else { + float scale = (float) BACKGROUND_WIDTH / width; + canvas.scale(scale, scale); + top = (BACKGROUND_HEIGHT - (int) (height * scale + 0.5)) / 2; + } + canvas.drawBitmap(bitmap, left, top, mPaint); + BoxBlurFilter.apply(target, + BoxBlurFilter.MODE_REPEAT, BoxBlurFilter.MODE_CLAMP); + return target; + } + + private void startTransition(Bitmap bitmap) { + BitmapTexture texture = new BitmapTexture(bitmap); + if (mBackground == null) { + mBackground = texture; + } else { + if (mOldBackground != null) mOldBackground.recycle(); + mOldBackground = mBackground; + mBackground = texture; + mAnimation.start(); + } + invalidate(); + } + + public void setImage(Bitmap bitmap) { + if (mAnimation.isActive()) { + mPendingBitmap = bitmap; + } else { + startTransition(bitmap); + } + } + + public void setScrollPosition(int position) { + if (mScrollX == position) return; + mScrollX = position; + invalidate(); + } + + @Override + protected void render(GLCanvas canvas) { + if (mBackground == null) return; + + int height = getHeight(); + float scale = (float) height / BACKGROUND_HEIGHT; + int width = (int) (BACKGROUND_WIDTH * scale + 0.5f); + int scroll = mScrollX; + int start = (scroll / width) * width; + + if (mOldBackground == null) { + for (int i = start, n = scroll + getWidth(); i < n; i += width) { + mBackground.draw(canvas, i - scroll, 0, width, height); + } + } else { + boolean moreAnimation = + mAnimation.calculate(canvas.currentAnimationTimeMillis()); + float ratio = mAnimation.get(); + for (int i = start, n = scroll + getWidth(); i < n; i += width) { + canvas.drawMixed(mOldBackground, + mBackground, ratio, i - scroll, 0, width, height); + } + if (moreAnimation) { + invalidate(); + } else if (mPendingBitmap != null) { + startTransition(mPendingBitmap); + mPendingBitmap = null; + } + } + } +} diff --git a/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java new file mode 100644 index 000000000..92d8b4156 --- /dev/null +++ b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java @@ -0,0 +1,543 @@ +/* + * 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.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.AlbumSetView.AlbumSetItem; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.MediaSetUtils; +import com.android.gallery3d.util.ThreadPool; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.os.Message; + +public class AlbumSetSlidingWindow implements AlbumSetView.ModelListener { + private static final String TAG = "GallerySlidingWindow"; + private static final int MSG_LOAD_BITMAP_DONE = 0; + + public static interface Listener { + public void onSizeChanged(int size); + public void onContentInvalidated(); + public void onWindowContentChanged( + int slot, AlbumSetItem old, AlbumSetItem update); + } + + private final AlbumSetView.Model mSource; + private int mSize; + private int mLabelWidth; + private int mDisplayItemSize; + private int mLabelFontSize; + + private int mContentStart = 0; + private int mContentEnd = 0; + + private int mActiveStart = 0; + private int mActiveEnd = 0; + + private Listener mListener; + + private final MyAlbumSetItem mData[]; + private SelectionDrawer mSelectionDrawer; + private final ColorTexture mWaitLoadingTexture; + + private SynchronizedHandler mHandler; + private ThreadPool mThreadPool; + + private int mActiveRequestCount = 0; + private String mLoadingLabel; + private boolean mIsActive = false; + + private static class MyAlbumSetItem extends AlbumSetItem { + public Path setPath; + public int sourceType; + public int cacheFlag; + public int cacheStatus; + } + + public AlbumSetSlidingWindow(GalleryActivity activity, int labelWidth, + int displayItemSize, int labelFontSize, SelectionDrawer drawer, + AlbumSetView.Model source, int cacheSize) { + source.setModelListener(this); + mLabelWidth = labelWidth; + mDisplayItemSize = displayItemSize; + mLabelFontSize = labelFontSize; + mLoadingLabel = activity.getAndroidContext().getString(R.string.loading); + mSource = source; + mSelectionDrawer = drawer; + mData = new MyAlbumSetItem[cacheSize]; + mSize = source.size(); + + mWaitLoadingTexture = new ColorTexture(Color.TRANSPARENT); + mWaitLoadingTexture.setSize(1, 1); + + mHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + Utils.assertTrue(message.what == MSG_LOAD_BITMAP_DONE); + ((GalleryDisplayItem) message.obj).onLoadBitmapDone(); + } + }; + + mThreadPool = activity.getThreadPool(); + } + + public void setSelectionDrawer(SelectionDrawer drawer) { + mSelectionDrawer = drawer; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public AlbumSetItem get(int slotIndex) { + Utils.assertTrue(isActiveSlot(slotIndex), + "invalid slot: %s outsides (%s, %s)", + slotIndex, mActiveStart, mActiveEnd); + return mData[slotIndex % mData.length]; + } + + public int size() { + return mSize; + } + + public boolean isActiveSlot(int slotIndex) { + return slotIndex >= mActiveStart && slotIndex < mActiveEnd; + } + + private void setContentWindow(int contentStart, int contentEnd) { + if (contentStart == mContentStart && contentEnd == mContentEnd) return; + + if (contentStart >= mContentEnd || mContentStart >= contentEnd) { + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + mSource.setActiveWindow(contentStart, contentEnd); + for (int i = contentStart; i < contentEnd; ++i) { + prepareSlotContent(i); + } + } else { + for (int i = mContentStart; i < contentStart; ++i) { + freeSlotContent(i); + } + for (int i = contentEnd, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + mSource.setActiveWindow(contentStart, contentEnd); + for (int i = contentStart, n = mContentStart; i < n; ++i) { + prepareSlotContent(i); + } + for (int i = mContentEnd; i < contentEnd; ++i) { + prepareSlotContent(i); + } + } + + mContentStart = contentStart; + mContentEnd = contentEnd; + } + + public void setActiveWindow(int start, int end) { + Utils.assertTrue( + start <= end && end - start <= mData.length && end <= mSize, + "start = %s, end = %s, length = %s, size = %s", + start, end, mData.length, mSize); + + AlbumSetItem data[] = mData; + + mActiveStart = start; + mActiveEnd = end; + + // If no data is visible, keep the cache content + if (start == end) return; + + int contentStart = Utils.clamp((start + end) / 2 - data.length / 2, + 0, Math.max(0, mSize - data.length)); + int contentEnd = Math.min(contentStart + data.length, mSize); + setContentWindow(contentStart, contentEnd); + if (mIsActive) updateAllImageRequests(); + } + + // We would like to request non active slots in the following order: + // Order: 8 6 4 2 1 3 5 7 + // |---------|---------------|---------| + // |<- active ->| + // |<-------- cached range ----------->| + private void requestNonactiveImages() { + int range = Math.max( + mContentEnd - mActiveEnd, mActiveStart - mContentStart); + for (int i = 0 ;i < range; ++i) { + requestImagesInSlot(mActiveEnd + i); + requestImagesInSlot(mActiveStart - 1 - i); + } + } + + private void cancelNonactiveImages() { + int range = Math.max( + mContentEnd - mActiveEnd, mActiveStart - mContentStart); + for (int i = 0 ;i < range; ++i) { + cancelImagesInSlot(mActiveEnd + i); + cancelImagesInSlot(mActiveStart - 1 - i); + } + } + + private void requestImagesInSlot(int slotIndex) { + if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; + AlbumSetItem items = mData[slotIndex % mData.length]; + for (DisplayItem item : items.covers) { + ((GalleryDisplayItem) item).requestImage(); + } + } + + private void cancelImagesInSlot(int slotIndex) { + if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; + AlbumSetItem items = mData[slotIndex % mData.length]; + for (DisplayItem item : items.covers) { + ((GalleryDisplayItem) item).cancelImageRequest(); + } + } + + private void freeSlotContent(int slotIndex) { + AlbumSetItem data[] = mData; + int index = slotIndex % data.length; + AlbumSetItem original = data[index]; + if (original != null) { + data[index] = null; + for (DisplayItem item : original.covers) { + ((GalleryDisplayItem) item).recycle(); + } + } + } + + private long getMediaSetDataVersion(MediaSet set) { + return set == null + ? MediaSet.INVALID_DATA_VERSION + : set.getDataVersion(); + } + + private void prepareSlotContent(int slotIndex) { + MediaSet set = mSource.getMediaSet(slotIndex); + + MyAlbumSetItem item = new MyAlbumSetItem(); + MediaItem[] coverItems = mSource.getCoverItems(slotIndex); + item.covers = new GalleryDisplayItem[coverItems.length]; + item.sourceType = identifySourceType(set); + item.cacheFlag = identifyCacheFlag(set); + item.cacheStatus = identifyCacheStatus(set); + item.setPath = set == null ? null : set.getPath(); + + for (int i = 0; i < coverItems.length; ++i) { + item.covers[i] = new GalleryDisplayItem(slotIndex, i, coverItems[i]); + } + item.labelItem = new LabelDisplayItem(slotIndex); + item.setDataVersion = getMediaSetDataVersion(set); + mData[slotIndex % mData.length] = item; + } + + private boolean isCoverItemsChanged(int slotIndex) { + AlbumSetItem original = mData[slotIndex % mData.length]; + if (original == null) return true; + MediaItem[] coverItems = mSource.getCoverItems(slotIndex); + + if (original.covers.length != coverItems.length) return true; + for (int i = 0, n = coverItems.length; i < n; ++i) { + GalleryDisplayItem g = (GalleryDisplayItem) original.covers[i]; + if (g.mDataVersion != coverItems[i].getDataVersion()) return true; + } + return false; + } + + private void updateSlotContent(final int slotIndex) { + + MyAlbumSetItem data[] = mData; + int pos = slotIndex % data.length; + MyAlbumSetItem original = data[pos]; + + if (!isCoverItemsChanged(slotIndex)) { + MediaSet set = mSource.getMediaSet(slotIndex); + original.sourceType = identifySourceType(set); + original.cacheFlag = identifyCacheFlag(set); + original.cacheStatus = identifyCacheStatus(set); + original.setPath = set == null ? null : set.getPath(); + ((LabelDisplayItem) original.labelItem).updateContent(); + if (mListener != null) mListener.onContentInvalidated(); + return; + } + + prepareSlotContent(slotIndex); + AlbumSetItem update = data[pos]; + + if (mListener != null && isActiveSlot(slotIndex)) { + mListener.onWindowContentChanged(slotIndex, original, update); + } + if (original != null) { + for (DisplayItem item : original.covers) { + ((GalleryDisplayItem) item).recycle(); + } + } + } + + private void notifySlotChanged(int slotIndex) { + // If the updated content is not cached, ignore it + if (slotIndex < mContentStart || slotIndex >= mContentEnd) { + Log.w(TAG, String.format( + "invalid update: %s is outside (%s, %s)", + slotIndex, mContentStart, mContentEnd) ); + return; + } + updateSlotContent(slotIndex); + boolean isActiveSlot = isActiveSlot(slotIndex); + if (mActiveRequestCount == 0 || isActiveSlot) { + for (DisplayItem item : mData[slotIndex % mData.length].covers) { + GalleryDisplayItem galleryItem = (GalleryDisplayItem) item; + galleryItem.requestImage(); + if (isActiveSlot && galleryItem.isRequestInProgress()) { + ++mActiveRequestCount; + } + } + } + } + + private void updateAllImageRequests() { + mActiveRequestCount = 0; + for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { + for (DisplayItem item : mData[i % mData.length].covers) { + GalleryDisplayItem coverItem = (GalleryDisplayItem) item; + coverItem.requestImage(); + if (coverItem.isRequestInProgress()) ++mActiveRequestCount; + } + } + if (mActiveRequestCount == 0) { + requestNonactiveImages(); + } else { + cancelNonactiveImages(); + } + } + + private class GalleryDisplayItem extends AbstractDisplayItem + implements FutureListener<Bitmap> { + private Future<Bitmap> mFuture; + private final int mSlotIndex; + private final int mCoverIndex; + private final int mMediaType; + private Texture mContent; + private final long mDataVersion; + + public GalleryDisplayItem(int slotIndex, int coverIndex, MediaItem item) { + super(item); + mSlotIndex = slotIndex; + mCoverIndex = coverIndex; + mMediaType = item.getMediaType(); + mDataVersion = item.getDataVersion(); + updateContent(mWaitLoadingTexture); + } + + @Override + protected void onBitmapAvailable(Bitmap bitmap) { + if (isActiveSlot(mSlotIndex)) { + --mActiveRequestCount; + if (mActiveRequestCount == 0) requestNonactiveImages(); + } + if (bitmap != null) { + BitmapTexture texture = new BitmapTexture(bitmap); + texture.setThrottled(true); + updateContent(texture); + if (mListener != null) mListener.onContentInvalidated(); + } + } + + private void updateContent(Texture content) { + mContent = content; + + int width = content.getWidth(); + int height = content.getHeight(); + + float scale = (float) mDisplayItemSize / Math.max(width, height); + + width = (int) Math.floor(width * scale); + height = (int) Math.floor(height * scale); + + setSize(width, height); + } + + @Override + public boolean render(GLCanvas canvas, int pass) { + int sourceType = SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED; + int cacheFlag = MediaSet.CACHE_FLAG_NO; + int cacheStatus = MediaSet.CACHE_STATUS_NOT_CACHED; + MyAlbumSetItem set = mData[mSlotIndex % mData.length]; + Path path = set.setPath; + if (mCoverIndex == 0) { + sourceType = set.sourceType; + cacheFlag = set.cacheFlag; + cacheStatus = set.cacheStatus; + } + + mSelectionDrawer.draw(canvas, mContent, mWidth, mHeight, + getRotation(), path, mCoverIndex, sourceType, mMediaType, + cacheFlag == MediaSet.CACHE_FLAG_FULL, + (cacheFlag == MediaSet.CACHE_FLAG_FULL) + && (cacheStatus != MediaSet.CACHE_STATUS_CACHED_FULL)); + return false; + } + + @Override + public void startLoadBitmap() { + mFuture = mThreadPool.submit(mMediaItem.requestImage( + MediaItem.TYPE_MICROTHUMBNAIL), this); + } + + @Override + public void cancelLoadBitmap() { + mFuture.cancel(); + } + + @Override + public void onFutureDone(Future<Bitmap> future) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_LOAD_BITMAP_DONE, this)); + } + + private void onLoadBitmapDone() { + Future<Bitmap> future = mFuture; + mFuture = null; + updateImage(future.get(), future.isCancelled()); + } + + @Override + public String toString() { + return String.format("GalleryDisplayItem(%s, %s)", mSlotIndex, mCoverIndex); + } + } + + private static int identifySourceType(MediaSet set) { + if (set == null) { + return SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED; + } + + Path path = set.getPath(); + if (MediaSetUtils.isCameraSource(path)) { + return SelectionDrawer.DATASOURCE_TYPE_CAMERA; + } + + int type = SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED; + String prefix = path.getPrefix(); + + if (prefix.equals("picasa")) { + type = SelectionDrawer.DATASOURCE_TYPE_PICASA; + } else if (prefix.equals("local") || prefix.equals("merge")) { + type = SelectionDrawer.DATASOURCE_TYPE_LOCAL; + } else if (prefix.equals("mtp")) { + type = SelectionDrawer.DATASOURCE_TYPE_MTP; + } + + return type; + } + + private static int identifyCacheFlag(MediaSet set) { + if (set == null || (set.getSupportedOperations() + & MediaSet.SUPPORT_CACHE) == 0) { + return MediaSet.CACHE_FLAG_NO; + } + + return set.getCacheFlag(); + } + + private static int identifyCacheStatus(MediaSet set) { + if (set == null || (set.getSupportedOperations() + & MediaSet.SUPPORT_CACHE) == 0) { + return MediaSet.CACHE_STATUS_NOT_CACHED; + } + + return set.getCacheStatus(); + } + + private class LabelDisplayItem extends DisplayItem { + private static final int FONT_COLOR = Color.WHITE; + + private StringTexture mTexture; + private String mLabel; + private String mPostfix; + private final int mSlotIndex; + + public LabelDisplayItem(int slotIndex) { + mSlotIndex = slotIndex; + updateContent(); + } + + public boolean updateContent() { + String label = mLoadingLabel; + String postfix = null; + MediaSet set = mSource.getMediaSet(mSlotIndex); + if (set != null) { + label = Utils.ensureNotNull(set.getName()); + postfix = " (" + set.getTotalMediaItemCount() + ")"; + } + if (Utils.equals(label, mLabel) + && Utils.equals(postfix, mPostfix)) return false; + mTexture = StringTexture.newInstance( + label, postfix, mLabelFontSize, FONT_COLOR, mLabelWidth, true); + setSize(mTexture.getWidth(), mTexture.getHeight()); + return true; + } + + @Override + public boolean render(GLCanvas canvas, int pass) { + mTexture.draw(canvas, -mWidth / 2, -mHeight / 2); + return false; + } + + @Override + public long getIdentity() { + return System.identityHashCode(this); + } + } + + public void onSizeChanged(int size) { + if (mSize != size) { + mSize = size; + if (mListener != null && mIsActive) mListener.onSizeChanged(mSize); + } + } + + public void onWindowContentChanged(int index) { + if (!mIsActive) { + // paused, ignore slot changed event + return; + } + notifySlotChanged(index); + } + + public void pause() { + mIsActive = false; + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + } + + public void resume() { + mIsActive = true; + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + prepareSlotContent(i); + } + updateAllImageRequests(); + } +} diff --git a/src/com/android/gallery3d/ui/AlbumSetView.java b/src/com/android/gallery3d/ui/AlbumSetView.java new file mode 100644 index 000000000..ef066b34c --- /dev/null +++ b/src/com/android/gallery3d/ui/AlbumSetView.java @@ -0,0 +1,240 @@ +/* + * 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.ui; + +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.ui.PositionRepository.Position; + +import android.graphics.Rect; + +import java.util.Random; + +public class AlbumSetView extends SlotView { + @SuppressWarnings("unused") + private static final String TAG = "AlbumSetView"; + private static final int CACHE_SIZE = 32; + private static final float PHOTO_DISTANCE = 35f; + + private int mVisibleStart; + private int mVisibleEnd; + + private Random mRandom = new Random(); + private long mSeed = mRandom.nextLong(); + + private AlbumSetSlidingWindow mDataWindow; + private final GalleryActivity mActivity; + private final int mSlotWidth; + private final int mDisplayItemSize; + private final int mLabelFontSize; + private final int mLabelOffsetY; + private final int mLabelMargin; + + private SelectionDrawer mSelectionDrawer; + + public static interface Model { + public MediaItem[] getCoverItems(int index); + public MediaSet getMediaSet(int index); + public int size(); + public void setActiveWindow(int start, int end); + public void setModelListener(ModelListener listener); + } + + public static interface ModelListener { + public void onWindowContentChanged(int index); + public void onSizeChanged(int size); + } + + public static class AlbumSetItem { + public DisplayItem[] covers; + public DisplayItem labelItem; + public long setDataVersion; + } + + public AlbumSetView(GalleryActivity activity, SelectionDrawer drawer, + int slotWidth, int slotHeight, int displayItemSize, + int labelFontSize, int labelOffsetY, int labelMargin) { + super(activity.getAndroidContext()); + mActivity = activity; + setSelectionDrawer(drawer); + setSlotSize(slotWidth, slotHeight); + mSlotWidth = slotWidth; + mDisplayItemSize = displayItemSize; + mLabelFontSize = labelFontSize; + mLabelOffsetY = labelOffsetY; + mLabelMargin = labelMargin; + } + + public void setSelectionDrawer(SelectionDrawer drawer) { + mSelectionDrawer = drawer; + if (mDataWindow != null) { + mDataWindow.setSelectionDrawer(drawer); + } + } + + public void setModel(AlbumSetView.Model model) { + if (mDataWindow != null) { + mDataWindow.setListener(null); + setSlotCount(0); + mDataWindow = null; + } + if (model != null) { + mDataWindow = new AlbumSetSlidingWindow(mActivity, + mSlotWidth - mLabelMargin * 2, mDisplayItemSize, mLabelFontSize, + mSelectionDrawer, model, CACHE_SIZE); + mDataWindow.setListener(new MyCacheListener()); + setSlotCount(mDataWindow.size()); + updateVisibleRange(getVisibleStart(), getVisibleEnd()); + } + } + + private void putSlotContent(int slotIndex, AlbumSetItem entry) { + // Get displayItems from mItemsetMap or create them from MediaSet. + Utils.assertTrue(entry != null); + Rect rect = getSlotRect(slotIndex); + + DisplayItem[] items = entry.covers; + mRandom.setSeed(slotIndex ^ mSeed); + + int x = (rect.left + rect.right) / 2; + int y = (rect.top + rect.bottom) / 2; + + Position basePosition = new Position(x, y, 0); + + // Put the cover items in reverse order, so that the first item is on + // top of the rest. + int labelY = y + mLabelOffsetY - entry.labelItem.getHeight() / 2; + Position position = new Position(x, labelY, 0f); + putDisplayItem(position, position, entry.labelItem); + + for (int i = 0, n = items.length; i < n; ++i) { + DisplayItem item = items[i]; + float dx = 0; + float dy = 0; + float dz = 0f; + float theta = 0; + if (i != 0) { + dz = i * PHOTO_DISTANCE; + } + position = new Position(x + dx, y + dy, dz); + position.theta = theta; + putDisplayItem(position, basePosition, item); + } + + } + + private void freeSlotContent(int index, AlbumSetItem entry) { + if (entry == null) return; + for (DisplayItem item : entry.covers) { + removeDisplayItem(item); + } + removeDisplayItem(entry.labelItem); + } + + public int size() { + return mDataWindow.size(); + } + + @Override + public void onLayoutChanged(int width, int height) { + updateVisibleRange(0, 0); + updateVisibleRange(getVisibleStart(), getVisibleEnd()); + } + + @Override + public void onScrollPositionChanged(int position) { + super.onScrollPositionChanged(position); + updateVisibleRange(getVisibleStart(), getVisibleEnd()); + } + + private void updateVisibleRange(int start, int end) { + if (start == mVisibleStart && end == mVisibleEnd) { + // we need to set the mDataWindow active range in any case. + mDataWindow.setActiveWindow(start, end); + return; + } + if (start >= mVisibleEnd || mVisibleStart >= end) { + for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) { + freeSlotContent(i, mDataWindow.get(i)); + } + mDataWindow.setActiveWindow(start, end); + for (int i = start; i < end; ++i) { + putSlotContent(i, mDataWindow.get(i)); + } + } else { + for (int i = mVisibleStart; i < start; ++i) { + freeSlotContent(i, mDataWindow.get(i)); + } + for (int i = end, n = mVisibleEnd; i < n; ++i) { + freeSlotContent(i, mDataWindow.get(i)); + } + mDataWindow.setActiveWindow(start, end); + for (int i = start, n = mVisibleStart; i < n; ++i) { + putSlotContent(i, mDataWindow.get(i)); + } + for (int i = mVisibleEnd; i < end; ++i) { + putSlotContent(i, mDataWindow.get(i)); + } + } + mVisibleStart = start; + mVisibleEnd = end; + + invalidate(); + } + + @Override + protected void render(GLCanvas canvas) { + mSelectionDrawer.prepareDrawing(); + super.render(canvas); + } + + private class MyCacheListener implements AlbumSetSlidingWindow.Listener { + + public void onSizeChanged(int size) { + // If the layout parameters are changed, we need reput all items. + if (setSlotCount(size)) updateVisibleRange(0, 0); + updateVisibleRange(getVisibleStart(), getVisibleEnd()); + invalidate(); + } + + public void onWindowContentChanged(int slot, AlbumSetItem old, AlbumSetItem update) { + freeSlotContent(slot, old); + putSlotContent(slot, update); + invalidate(); + } + + public void onContentInvalidated() { + invalidate(); + } + } + + public void pause() { + for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) { + freeSlotContent(i, mDataWindow.get(i)); + } + mDataWindow.pause(); + } + + public void resume() { + mDataWindow.resume(); + for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) { + putSlotContent(i, mDataWindow.get(i)); + } + } +} diff --git a/src/com/android/gallery3d/ui/AlbumSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java new file mode 100644 index 000000000..9e44bd1d2 --- /dev/null +++ b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java @@ -0,0 +1,433 @@ +/* + * 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.ui; + +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.common.LruCache; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.ThreadPool; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.os.Message; + +public class AlbumSlidingWindow implements AlbumView.ModelListener { + @SuppressWarnings("unused") + private static final String TAG = "AlbumSlidingWindow"; + + private static final int MSG_LOAD_BITMAP_DONE = 0; + private static final int MSG_UPDATE_SLOT = 1; + private static final int MIN_THUMB_SIZE = 100; + + public static interface Listener { + public void onSizeChanged(int size); + public void onContentInvalidated(); + public void onWindowContentChanged( + int slot, DisplayItem old, DisplayItem update); + } + + private final AlbumView.Model mSource; + private int mSize; + + private int mContentStart = 0; + private int mContentEnd = 0; + + private int mActiveStart = 0; + private int mActiveEnd = 0; + + private Listener mListener; + private int mFocusIndex = -1; + + private final AlbumDisplayItem mData[]; + private final ColorTexture mWaitLoadingTexture; + private SelectionDrawer mSelectionDrawer; + + private SynchronizedHandler mHandler; + private ThreadPool mThreadPool; + private int mSlotWidth, mSlotHeight; + + private int mActiveRequestCount = 0; + private boolean mIsActive = false; + + private int mDisplayItemSize; // 0: disabled + private LruCache<Path, Bitmap> mImageCache = new LruCache<Path, Bitmap>(1000); + + public AlbumSlidingWindow(GalleryActivity activity, + AlbumView.Model source, int cacheSize, + int slotWidth, int slotHeight, int displayItemSize) { + source.setModelListener(this); + mSource = source; + mData = new AlbumDisplayItem[cacheSize]; + mSize = source.size(); + mSlotWidth = slotWidth; + mSlotHeight = slotHeight; + mDisplayItemSize = displayItemSize; + + mWaitLoadingTexture = new ColorTexture(Color.TRANSPARENT); + mWaitLoadingTexture.setSize(1, 1); + + mHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_LOAD_BITMAP_DONE: { + ((AlbumDisplayItem) message.obj).onLoadBitmapDone(); + break; + } + case MSG_UPDATE_SLOT: { + updateSlotContent(message.arg1); + break; + } + } + } + }; + + mThreadPool = activity.getThreadPool(); + } + + public void setSelectionDrawer(SelectionDrawer drawer) { + mSelectionDrawer = drawer; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public void setFocusIndex(int slotIndex) { + mFocusIndex = slotIndex; + } + + public DisplayItem get(int slotIndex) { + Utils.assertTrue(isActiveSlot(slotIndex), + "invalid slot: %s outsides (%s, %s)", + slotIndex, mActiveStart, mActiveEnd); + return mData[slotIndex % mData.length]; + } + + public int size() { + return mSize; + } + + public boolean isActiveSlot(int slotIndex) { + return slotIndex >= mActiveStart && slotIndex < mActiveEnd; + } + + private void setContentWindow(int contentStart, int contentEnd) { + if (contentStart == mContentStart && contentEnd == mContentEnd) return; + + if (!mIsActive) { + mContentStart = contentStart; + mContentEnd = contentEnd; + mSource.setActiveWindow(contentStart, contentEnd); + return; + } + + if (contentStart >= mContentEnd || mContentStart >= contentEnd) { + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + mSource.setActiveWindow(contentStart, contentEnd); + for (int i = contentStart; i < contentEnd; ++i) { + prepareSlotContent(i); + } + } else { + for (int i = mContentStart; i < contentStart; ++i) { + freeSlotContent(i); + } + for (int i = contentEnd, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + mSource.setActiveWindow(contentStart, contentEnd); + for (int i = contentStart, n = mContentStart; i < n; ++i) { + prepareSlotContent(i); + } + for (int i = mContentEnd; i < contentEnd; ++i) { + prepareSlotContent(i); + } + } + + mContentStart = contentStart; + mContentEnd = contentEnd; + } + + public void setActiveWindow(int start, int end) { + Utils.assertTrue(start <= end + && end - start <= mData.length && end <= mSize, + "%s, %s, %s, %s", start, end, mData.length, mSize); + DisplayItem data[] = mData; + + mActiveStart = start; + mActiveEnd = end; + + // If no data is visible, keep the cache content + if (start == end) return; + + int contentStart = Utils.clamp((start + end) / 2 - data.length / 2, + 0, Math.max(0, mSize - data.length)); + int contentEnd = Math.min(contentStart + data.length, mSize); + setContentWindow(contentStart, contentEnd); + if (mIsActive) updateAllImageRequests(); + } + + // We would like to request non active slots in the following order: + // Order: 8 6 4 2 1 3 5 7 + // |---------|---------------|---------| + // |<- active ->| + // |<-------- cached range ----------->| + private void requestNonactiveImages() { + int range = Math.max( + (mContentEnd - mActiveEnd), (mActiveStart - mContentStart)); + for (int i = 0 ;i < range; ++i) { + requestSlotImage(mActiveEnd + i, false); + requestSlotImage(mActiveStart - 1 - i, false); + } + } + + private void requestSlotImage(int slotIndex, boolean isActive) { + if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; + AlbumDisplayItem item = mData[slotIndex % mData.length]; + item.requestImage(); + } + + private void cancelNonactiveImages() { + int range = Math.max( + (mContentEnd - mActiveEnd), (mActiveStart - mContentStart)); + for (int i = 0 ;i < range; ++i) { + cancelSlotImage(mActiveEnd + i, false); + cancelSlotImage(mActiveStart - 1 - i, false); + } + } + + private void cancelSlotImage(int slotIndex, boolean isActive) { + if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; + AlbumDisplayItem item = mData[slotIndex % mData.length]; + item.cancelImageRequest(); + } + + private void freeSlotContent(int slotIndex) { + AlbumDisplayItem data[] = mData; + int index = slotIndex % data.length; + AlbumDisplayItem original = data[index]; + if (original != null) { + original.recycle(); + data[index] = null; + } + } + + private void prepareSlotContent(final int slotIndex) { + mData[slotIndex % mData.length] = new AlbumDisplayItem( + slotIndex, mSource.get(slotIndex)); + } + + private void updateSlotContent(final int slotIndex) { + MediaItem item = mSource.get(slotIndex); + AlbumDisplayItem data[] = mData; + int index = slotIndex % data.length; + AlbumDisplayItem original = data[index]; + AlbumDisplayItem update = new AlbumDisplayItem(slotIndex, item); + data[index] = update; + boolean isActive = isActiveSlot(slotIndex); + if (mListener != null && isActive) { + mListener.onWindowContentChanged(slotIndex, original, update); + } + if (original != null) { + if (isActive && original.isRequestInProgress()) { + --mActiveRequestCount; + } + original.recycle(); + } + if (isActive) { + if (mActiveRequestCount == 0) cancelNonactiveImages(); + ++mActiveRequestCount; + update.requestImage(); + } else { + if (mActiveRequestCount == 0) update.requestImage(); + } + } + + private void updateAllImageRequests() { + mActiveRequestCount = 0; + AlbumDisplayItem data[] = mData; + for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { + AlbumDisplayItem item = data[i % data.length]; + item.requestImage(); + if (item.isRequestInProgress()) ++mActiveRequestCount; + } + if (mActiveRequestCount == 0) { + requestNonactiveImages(); + } else { + cancelNonactiveImages(); + } + } + + private class AlbumDisplayItem extends AbstractDisplayItem + implements FutureListener<Bitmap>, Job<Bitmap> { + private Future<Bitmap> mFuture; + private final int mSlotIndex; + private final int mMediaType; + private Texture mContent; + + public AlbumDisplayItem(int slotIndex, MediaItem item) { + super(item); + mMediaType = (item == null) + ? MediaItem.MEDIA_TYPE_UNKNOWN + : item.getMediaType(); + mSlotIndex = slotIndex; + updateContent(mWaitLoadingTexture); + } + + @Override + protected void onBitmapAvailable(Bitmap bitmap) { + boolean isActiveSlot = isActiveSlot(mSlotIndex); + if (isActiveSlot) { + --mActiveRequestCount; + if (mActiveRequestCount == 0) requestNonactiveImages(); + } + if (bitmap != null) { + BitmapTexture texture = new BitmapTexture(bitmap); + texture.setThrottled(true); + updateContent(texture); + if (mListener != null && isActiveSlot) { + mListener.onContentInvalidated(); + } + } + } + + private void updateContent(Texture content) { + mContent = content; + + int width = mContent.getWidth(); + int height = mContent.getHeight(); + + float scalex = mDisplayItemSize / (float) width; + float scaley = mDisplayItemSize / (float) height; + float scale = Math.min(scalex, scaley); + + width = (int) Math.floor(width * scale); + height = (int) Math.floor(height * scale); + + setSize(width, height); + } + + @Override + public boolean render(GLCanvas canvas, int pass) { + if (pass == 0) { + Path path = null; + if (mMediaItem != null) path = mMediaItem.getPath(); + mSelectionDrawer.draw(canvas, mContent, mWidth, mHeight, + getRotation(), path, mMediaType); + return (mFocusIndex == mSlotIndex); + } else if (pass == 1) { + mSelectionDrawer.drawFocus(canvas, mWidth, mHeight); + } + return false; + } + + @Override + public void startLoadBitmap() { + if (mDisplayItemSize < MIN_THUMB_SIZE) { + Path path = mMediaItem.getPath(); + if (mImageCache.containsKey(path)) { + Bitmap bitmap = mImageCache.get(path); + updateImage(bitmap, false); + return; + } + mFuture = mThreadPool.submit(this, this); + } else { + mFuture = mThreadPool.submit(mMediaItem.requestImage( + MediaItem.TYPE_MICROTHUMBNAIL), this); + } + } + + // This gets the bitmap and scale it down. + public Bitmap run(JobContext jc) { + Job<Bitmap> job = mMediaItem.requestImage( + MediaItem.TYPE_MICROTHUMBNAIL); + Bitmap bitmap = job.run(jc); + if (bitmap != null) { + bitmap = BitmapUtils.resizeDownBySideLength( + bitmap, mDisplayItemSize, true); + } + return bitmap; + } + + @Override + public void cancelLoadBitmap() { + if (mFuture != null) { + mFuture.cancel(); + } + } + + @Override + public void onFutureDone(Future<Bitmap> bitmap) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_LOAD_BITMAP_DONE, this)); + } + + private void onLoadBitmapDone() { + Future<Bitmap> future = mFuture; + mFuture = null; + Bitmap bitmap = future.get(); + boolean isCancelled = future.isCancelled(); + if (mDisplayItemSize < MIN_THUMB_SIZE && (bitmap != null || !isCancelled)) { + Path path = mMediaItem.getPath(); + mImageCache.put(path, bitmap); + } + updateImage(bitmap, isCancelled); + } + + @Override + public String toString() { + return String.format("AlbumDisplayItem[%s]", mSlotIndex); + } + } + + public void onSizeChanged(int size) { + if (mSize != size) { + mSize = size; + if (mListener != null) mListener.onSizeChanged(mSize); + } + } + + public void onWindowContentChanged(int index) { + if (index >= mContentStart && index < mContentEnd && mIsActive) { + updateSlotContent(index); + } + } + + public void resume() { + mIsActive = true; + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + prepareSlotContent(i); + } + updateAllImageRequests(); + } + + public void pause() { + mIsActive = false; + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + mImageCache.clear(); + } +} diff --git a/src/com/android/gallery3d/ui/AlbumView.java b/src/com/android/gallery3d/ui/AlbumView.java new file mode 100644 index 000000000..417611a69 --- /dev/null +++ b/src/com/android/gallery3d/ui/AlbumView.java @@ -0,0 +1,197 @@ +/* + * 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.ui; + +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.ui.PositionRepository.Position; + +import android.graphics.Rect; + +public class AlbumView extends SlotView { + @SuppressWarnings("unused") + private static final String TAG = "AlbumView"; + private static final int CACHE_SIZE = 64; + + private int mVisibleStart = 0; + private int mVisibleEnd = 0; + + private AlbumSlidingWindow mDataWindow; + private final GalleryActivity mActivity; + private SelectionDrawer mSelectionDrawer; + private int mSlotWidth, mSlotHeight; + private int mDisplayItemSize; + + private boolean mIsActive = false; + + public static interface Model { + public int size(); + public MediaItem get(int index); + public void setActiveWindow(int start, int end); + public void setModelListener(ModelListener listener); + } + + public static interface ModelListener { + public void onWindowContentChanged(int index); + public void onSizeChanged(int size); + } + + public AlbumView(GalleryActivity activity, + int slotWidth, int slotHeight, int displayItemSize) { + super(activity.getAndroidContext()); + mSlotWidth = slotWidth; + mSlotHeight = slotHeight; + mDisplayItemSize = displayItemSize; + setSlotSize(slotWidth, slotHeight); + mActivity = activity; + } + + public void setSelectionDrawer(SelectionDrawer drawer) { + mSelectionDrawer = drawer; + if (mDataWindow != null) mDataWindow.setSelectionDrawer(drawer); + } + + public void setModel(Model model) { + if (mDataWindow != null) { + mDataWindow.setListener(null); + setSlotCount(0); + mDataWindow = null; + } + if (model != null) { + mDataWindow = new AlbumSlidingWindow( + mActivity, model, CACHE_SIZE, + mSlotWidth, mSlotHeight, mDisplayItemSize); + mDataWindow.setSelectionDrawer(mSelectionDrawer); + mDataWindow.setListener(new MyDataModelListener()); + setSlotCount(model.size()); + updateVisibleRange(getVisibleStart(), getVisibleEnd()); + } + } + + public void setFocusIndex(int slotIndex) { + if (mDataWindow != null) { + mDataWindow.setFocusIndex(slotIndex); + } + } + + private void putSlotContent(int slotIndex, DisplayItem item) { + Rect rect = getSlotRect(slotIndex); + Position position = new Position( + (rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2, 0); + putDisplayItem(position, position, item); + } + + private void updateVisibleRange(int start, int end) { + if (start == mVisibleStart && end == mVisibleEnd) { + // we need to set the mDataWindow active range in any case. + mDataWindow.setActiveWindow(start, end); + return; + } + + if (!mIsActive) { + mVisibleStart = start; + mVisibleEnd = end; + mDataWindow.setActiveWindow(start, end); + return; + } + + if (start >= mVisibleEnd || mVisibleStart >= end) { + for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) { + DisplayItem item = mDataWindow.get(i); + if (item != null) removeDisplayItem(item); + } + mDataWindow.setActiveWindow(start, end); + for (int i = start; i < end; ++i) { + putSlotContent(i, mDataWindow.get(i)); + } + } else { + for (int i = mVisibleStart; i < start; ++i) { + DisplayItem item = mDataWindow.get(i); + if (item != null) removeDisplayItem(item); + } + for (int i = end, n = mVisibleEnd; i < n; ++i) { + DisplayItem item = mDataWindow.get(i); + if (item != null) removeDisplayItem(item); + } + mDataWindow.setActiveWindow(start, end); + for (int i = start, n = mVisibleStart; i < n; ++i) { + putSlotContent(i, mDataWindow.get(i)); + } + for (int i = mVisibleEnd; i < end; ++i) { + putSlotContent(i, mDataWindow.get(i)); + } + } + + mVisibleStart = start; + mVisibleEnd = end; + } + + @Override + protected void onLayoutChanged(int width, int height) { + // Reput all the items + updateVisibleRange(0, 0); + updateVisibleRange(getVisibleStart(), getVisibleEnd()); + } + + @Override + protected void onScrollPositionChanged(int position) { + super.onScrollPositionChanged(position); + updateVisibleRange(getVisibleStart(), getVisibleEnd()); + } + + @Override + protected void render(GLCanvas canvas) { + mSelectionDrawer.prepareDrawing(); + super.render(canvas); + } + + private class MyDataModelListener implements AlbumSlidingWindow.Listener { + + public void onContentInvalidated() { + invalidate(); + } + + public void onSizeChanged(int size) { + // If the layout parameters are changed, we need reput all items. + if (setSlotCount(size)) updateVisibleRange(0, 0); + updateVisibleRange(getVisibleStart(), getVisibleEnd()); + invalidate(); + } + + public void onWindowContentChanged( + int slotIndex, DisplayItem old, DisplayItem update) { + removeDisplayItem(old); + putSlotContent(slotIndex, update); + } + } + + public void resume() { + mIsActive = true; + mDataWindow.resume(); + for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) { + putSlotContent(i, mDataWindow.get(i)); + } + } + + public void pause() { + mIsActive = false; + for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) { + removeDisplayItem(mDataWindow.get(i)); + } + mDataWindow.pause(); + } +} diff --git a/src/com/android/gallery3d/ui/BasicTexture.java b/src/com/android/gallery3d/ui/BasicTexture.java new file mode 100644 index 000000000..e93006326 --- /dev/null +++ b/src/com/android/gallery3d/ui/BasicTexture.java @@ -0,0 +1,164 @@ +/* + * 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.ui; + +import com.android.gallery3d.common.Utils; + +import java.lang.ref.WeakReference; +import java.util.WeakHashMap; + +// BasicTexture is a Texture corresponds to a real GL texture. +// The state of a BasicTexture indicates whether its data is loaded to GL memory. +// If a BasicTexture is loaded into GL memory, it has a GL texture id. +abstract class BasicTexture implements Texture { + + @SuppressWarnings("unused") + private static final String TAG = "BasicTexture"; + protected static final int UNSPECIFIED = -1; + + protected static final int STATE_UNLOADED = 0; + protected static final int STATE_LOADED = 1; + protected static final int STATE_ERROR = -1; + + protected int mId; + protected int mState; + + protected int mWidth = UNSPECIFIED; + protected int mHeight = UNSPECIFIED; + + private int mTextureWidth; + private int mTextureHeight; + + protected WeakReference<GLCanvas> mCanvasRef = null; + private static WeakHashMap<BasicTexture, Object> sAllTextures + = new WeakHashMap<BasicTexture, Object>(); + private static ThreadLocal sInFinalizer = new ThreadLocal(); + + protected BasicTexture(GLCanvas canvas, int id, int state) { + setAssociatedCanvas(canvas); + mId = id; + mState = state; + synchronized (sAllTextures) { + sAllTextures.put(this, null); + } + } + + protected BasicTexture() { + this(null, 0, STATE_UNLOADED); + } + + protected void setAssociatedCanvas(GLCanvas canvas) { + mCanvasRef = canvas == null + ? null + : new WeakReference<GLCanvas>(canvas); + } + + /** + * Sets the content size of this texture. In OpenGL, the actual texture + * size must be of power of 2, the size of the content may be smaller. + */ + protected void setSize(int width, int height) { + mWidth = width; + mHeight = height; + mTextureWidth = Utils.nextPowerOf2(width); + mTextureHeight = Utils.nextPowerOf2(height); + } + + public int getId() { + return mId; + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + + // Returns the width rounded to the next power of 2. + public int getTextureWidth() { + return mTextureWidth; + } + + // Returns the height rounded to the next power of 2. + public int getTextureHeight() { + return mTextureHeight; + } + + public void draw(GLCanvas canvas, int x, int y) { + canvas.drawTexture(this, x, y, getWidth(), getHeight()); + } + + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + canvas.drawTexture(this, x, y, w, h); + } + + // onBind is called before GLCanvas binds this texture. + // It should make sure the data is uploaded to GL memory. + abstract protected boolean onBind(GLCanvas canvas); + + public boolean isLoaded(GLCanvas canvas) { + return mState == STATE_LOADED && mCanvasRef.get() == canvas; + } + + // recycle() is called when the texture will never be used again, + // so it can free all resources. + public void recycle() { + freeResource(); + } + + // yield() is called when the texture will not be used temporarily, + // so it can free some resources. + // The default implementation unloads the texture from GL memory, so + // the subclass should make sure it can reload the texture to GL memory + // later, or it will have to override this method. + public void yield() { + freeResource(); + } + + private void freeResource() { + GLCanvas canvas = mCanvasRef == null ? null : mCanvasRef.get(); + if (canvas != null && isLoaded(canvas)) { + canvas.unloadTexture(this); + } + mState = BasicTexture.STATE_UNLOADED; + setAssociatedCanvas(null); + } + + @Override + protected void finalize() { + sInFinalizer.set(BasicTexture.class); + recycle(); + sInFinalizer.set(null); + } + + // This is for deciding if we can call Bitmap's recycle(). + // We cannot call Bitmap's recycle() in finalizer because at that point + // the finalizer of Bitmap may already be called so recycle() will crash. + public static boolean inFinalizer() { + return sInFinalizer.get() != null; + } + + public static void yieldAllTextures() { + synchronized (sAllTextures) { + for (BasicTexture t : sAllTextures.keySet()) { + t.yield(); + } + } + } +} diff --git a/src/com/android/gallery3d/ui/BitmapTexture.java b/src/com/android/gallery3d/ui/BitmapTexture.java new file mode 100644 index 000000000..046bda94c --- /dev/null +++ b/src/com/android/gallery3d/ui/BitmapTexture.java @@ -0,0 +1,49 @@ +/* + * 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.ui; + +import com.android.gallery3d.common.Utils; + +import android.graphics.Bitmap; + +// BitmapTexture is a texture whose content is specified by a fixed Bitmap. +// +// The texture does not own the Bitmap. The user should make sure the Bitmap +// is valid during the texture's lifetime. When the texture is recycled, it +// does not free the Bitmap. +public class BitmapTexture extends UploadedTexture { + protected Bitmap mContentBitmap; + + public BitmapTexture(Bitmap bitmap) { + Utils.assertTrue(bitmap != null && !bitmap.isRecycled()); + mContentBitmap = bitmap; + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + // Do nothing. + } + + @Override + protected Bitmap onGetBitmap() { + return mContentBitmap; + } + + public Bitmap getBitmap() { + return mContentBitmap; + } +} diff --git a/src/com/android/gallery3d/ui/BitmapTileProvider.java b/src/com/android/gallery3d/ui/BitmapTileProvider.java new file mode 100644 index 000000000..a47337fa2 --- /dev/null +++ b/src/com/android/gallery3d/ui/BitmapTileProvider.java @@ -0,0 +1,91 @@ +/* + * 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.ui; + +import com.android.gallery3d.common.BitmapUtils; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Bitmap.Config; + +import java.util.ArrayList; + +public class BitmapTileProvider implements TileImageView.Model { + private final Bitmap mBackup; + private final Bitmap[] mMipmaps; + private final Config mConfig; + private final int mImageWidth; + private final int mImageHeight; + + private boolean mRecycled = false; + + public BitmapTileProvider(Bitmap bitmap, int maxBackupSize) { + mImageWidth = bitmap.getWidth(); + mImageHeight = bitmap.getHeight(); + ArrayList<Bitmap> list = new ArrayList<Bitmap>(); + list.add(bitmap); + while (bitmap.getWidth() > maxBackupSize + || bitmap.getHeight() > maxBackupSize) { + bitmap = BitmapUtils.resizeBitmapByScale(bitmap, 0.5f, false); + list.add(bitmap); + } + + mBackup = list.remove(list.size() - 1); + mMipmaps = list.toArray(new Bitmap[list.size()]); + mConfig = Config.ARGB_8888; + } + + public Bitmap getBackupImage() { + return mBackup; + } + + public int getImageHeight() { + return mImageHeight; + } + + public int getImageWidth() { + return mImageWidth; + } + + public int getLevelCount() { + return mMipmaps.length; + } + + public Bitmap getTile(int level, int x, int y, int tileSize) { + Bitmap result = Bitmap.createBitmap(tileSize, tileSize, mConfig); + Canvas canvas = new Canvas(result); + canvas.drawBitmap(mMipmaps[level], -(x >> level), -(y >> level), null); + return result; + } + + public void recycle() { + if (mRecycled) return; + mRecycled = true; + for (Bitmap bitmap : mMipmaps) { + BitmapUtils.recycleSilently(bitmap); + } + BitmapUtils.recycleSilently(mBackup); + } + + public int getRotation() { + return 0; + } + + public boolean isFailedToLoad() { + return false; + } +} diff --git a/src/com/android/gallery3d/ui/BoxBlurFilter.java b/src/com/android/gallery3d/ui/BoxBlurFilter.java new file mode 100644 index 000000000..0497a61fa --- /dev/null +++ b/src/com/android/gallery3d/ui/BoxBlurFilter.java @@ -0,0 +1,100 @@ +/* + * 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.ui; + +import android.graphics.Bitmap; + + +public class BoxBlurFilter { + private static final int RED_MASK = 0xff0000; + private static final int RED_MASK_SHIFT = 16; + private static final int GREEN_MASK = 0x00ff00; + private static final int GREEN_MASK_SHIFT = 8; + private static final int BLUE_MASK = 0x0000ff; + private static final int RADIUS = 4; + private static final int KERNEL_SIZE = RADIUS * 2 + 1; + private static final int NUM_COLORS = 256; + private static final int[] KERNEL_NORM = new int[KERNEL_SIZE * NUM_COLORS]; + + public static final int MODE_REPEAT = 1; + public static final int MODE_CLAMP = 2; + + static { + int index = 0; + // Build a lookup table from summed to normalized kernel values. + // The formula: KERNAL_NORM[value] = value / KERNEL_SIZE + for (int i = 0; i < NUM_COLORS; ++i) { + for (int j = 0; j < KERNEL_SIZE; ++j) { + KERNEL_NORM[index++] = i; + } + } + } + + private BoxBlurFilter() { + } + + private static int sample(int x, int width, int mode) { + if (x >= 0 && x < width) return x; + return mode == MODE_REPEAT + ? x < 0 ? x + width : x - width + : x < 0 ? 0 : width - 1; + } + + public static void apply( + Bitmap bitmap, int horizontalMode, int verticalMode) { + + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + int data[] = new int[width * height]; + bitmap.getPixels(data, 0, width, 0, 0, width, height); + int temp[] = new int[width * height]; + applyOneDimension(data, temp, width, height, horizontalMode); + applyOneDimension(temp, data, height, width, verticalMode); + bitmap.setPixels(data, 0, width, 0, 0, width, height); + } + + private static void applyOneDimension( + int[] in, int[] out, int width, int height, int mode) { + for (int y = 0, read = 0; y < height; ++y, read += width) { + // Evaluate the kernel for the first pixel in the row. + int red = 0; + int green = 0; + int blue = 0; + for (int i = -RADIUS; i <= RADIUS; ++i) { + int argb = in[read + sample(i, width, mode)]; + red += (argb & RED_MASK) >> RED_MASK_SHIFT; + green += (argb & GREEN_MASK) >> GREEN_MASK_SHIFT; + blue += argb & BLUE_MASK; + } + for (int x = 0, write = y; x < width; ++x, write += height) { + // Output the current pixel. + out[write] = 0xFF000000 + | (KERNEL_NORM[red] << RED_MASK_SHIFT) + | (KERNEL_NORM[green] << GREEN_MASK_SHIFT) + | KERNEL_NORM[blue]; + + // Slide to the next pixel, adding the new rightmost pixel and + // subtracting the former leftmost. + int prev = in[read + sample(x - RADIUS, width, mode)]; + int next = in[read + sample(x + RADIUS + 1, width, mode)]; + red += ((next & RED_MASK) - (prev & RED_MASK)) >> RED_MASK_SHIFT; + green += ((next & GREEN_MASK) - (prev & GREEN_MASK)) >> GREEN_MASK_SHIFT; + blue += (next & BLUE_MASK) - (prev & BLUE_MASK); + } + } + } +} diff --git a/src/com/android/gallery3d/ui/CacheBarView.java b/src/com/android/gallery3d/ui/CacheBarView.java new file mode 100644 index 000000000..40f84d8f9 --- /dev/null +++ b/src/com/android/gallery3d/ui/CacheBarView.java @@ -0,0 +1,270 @@ +/* + * 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.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Message; +import android.os.StatFs; +import android.text.format.Formatter; +import android.view.View.MeasureSpec; + +import java.io.File; + +public class CacheBarView extends GLView implements TextButton.OnClickedListener { + private static final String TAG = "CacheBarView"; + private static final int FONT_COLOR = 0xffffffff; + private static final int MSG_REFRESH_STORAGE = 1; + private static final int PIN_SIZE = 36; + + public interface Listener { + void onDoneClicked(); + } + + private GalleryActivity mActivity; + private Context mContext; + + private StorageInfo mStorageInfo; + private long mUserChangeDelta; + private Future<StorageInfo> mStorageInfoFuture; + private Handler mHandler; + + private int mTotalHeight; + private int mPinLeftMargin; + private int mPinRightMargin; + private int mButtonRightMargin; + + private NinePatchTexture mBackground; + private GLView mLeftPin; // The pin icon. + private GLView mLeftLabel; // "Make available offline" + private ProgressBar mStorageBar; + private Label mStorageLabel; // "27.26 GB free" + private TextButton mDoneButton; // "Done" + + private Listener mListener; + + public CacheBarView(GalleryActivity activity, int resBackground, int height, + int pinLeftMargin, int pinRightMargin, int buttonRightMargin, + int fontSize) { + mActivity = activity; + mContext = activity.getAndroidContext(); + + mPinLeftMargin = pinLeftMargin; + mPinRightMargin = pinRightMargin; + mButtonRightMargin = buttonRightMargin; + + mBackground = new NinePatchTexture(mContext, resBackground); + Rect paddings = mBackground.getPaddings(); + + // The total height of the strip that includes the bar containing Pin, + // Label, DoneButton, ..., ect. and the extended fading bar. + mTotalHeight = height + paddings.top; + + mLeftPin = new Icon(mContext, R.drawable.ic_manage_pin, PIN_SIZE, PIN_SIZE); + mLeftLabel = new Label(mContext, R.string.make_available_offline, + fontSize, FONT_COLOR); + addComponent(mLeftPin); + addComponent(mLeftLabel); + + mDoneButton = new TextButton(mContext, R.string.done); + mDoneButton.setOnClickListener(this); + NinePatchTexture normal = new NinePatchTexture( + mContext, R.drawable.btn_default_normal_holo_dark); + NinePatchTexture pressed = new NinePatchTexture( + mContext, R.drawable.btn_default_pressed_holo_dark); + mDoneButton.setNormalBackground(normal); + mDoneButton.setPressedBackground(pressed); + addComponent(mDoneButton); + + // Initially the progress bar and label are invisible. + // It will be made visible after we have the storage information. + mStorageBar = new ProgressBar(mContext, + R.drawable.progress_primary_holo_dark, + R.drawable.progress_secondary_holo_dark, + R.drawable.progress_bg_holo_dark); + mStorageLabel = new Label(mContext, "", 14, Color.WHITE); + addComponent(mStorageBar); + addComponent(mStorageLabel); + mStorageBar.setVisibility(GLView.INVISIBLE); + mStorageLabel.setVisibility(GLView.INVISIBLE); + + mHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + public void handleMessage(Message msg) { + switch(msg.what) { + case MSG_REFRESH_STORAGE: + mStorageInfo = (StorageInfo) msg.obj; + refreshStorageInfo(); + break; + } + } + }; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + // Called by mDoneButton + public void onClicked(GLView source) { + if (mListener != null) { + mListener.onDoneClicked(); + } + } + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + // The size of mStorageLabel can change, so we need to layout + // even if the size of CacheBarView does not change. + int w = right - left; + int h = bottom - top; + + mLeftPin.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + int pinH = mLeftPin.getMeasuredHeight(); + int pinW = mLeftPin.getMeasuredWidth(); + int pinT = (h - pinH) / 2; + int pinL = mPinLeftMargin; + mLeftPin.layout(pinL, pinT, pinL + pinW, pinT + pinH); + + mLeftLabel.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + int labelH = mLeftLabel.getMeasuredHeight(); + int labelW = mLeftLabel.getMeasuredWidth(); + int labelT = (h - labelH) / 2; + int labelL = pinL + pinW + mPinRightMargin; + mLeftLabel.layout(labelL, labelT, labelL + labelW, labelT + labelH); + + mDoneButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + int doneH = mDoneButton.getMeasuredHeight(); + int doneW = mDoneButton.getMeasuredWidth(); + int doneT = (h - doneH) / 2; + int doneR = w - mButtonRightMargin; + mDoneButton.layout(doneR - doneW, doneT, doneR, doneT + doneH); + + int centerX = w / 2; + int centerY = h / 2; + + int capBarH = 20; + int capBarW = 200; + int capBarT = centerY - capBarH / 2; + int capBarL = centerX - capBarW / 2; + mStorageBar.layout(capBarL, capBarT, capBarL + capBarW, + capBarT + capBarH); + + mStorageLabel.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + int capLabelH = mStorageLabel.getMeasuredHeight(); + int capLabelW = mStorageLabel.getMeasuredWidth(); + int capLabelT = centerY - capLabelH / 2; + int capLabelL = centerX + capBarW / 2 + 8; + mStorageLabel.layout(capLabelL , capLabelT, capLabelL + capLabelW, + capLabelT + capLabelH); + } + + public void refreshStorageInfo() { + long used = mStorageInfo.usedBytes; + long total = mStorageInfo.totalBytes; + long cached = mStorageInfo.usedCacheBytes; + long target = mStorageInfo.targetCacheBytes; + + double primary = (double) used / total; + double secondary = + (double) (used - cached + target + mUserChangeDelta) / total; + + mStorageBar.setProgress((int) (primary * 10000)); + mStorageBar.setSecondaryProgress((int) (secondary * 10000)); + + long freeBytes = mStorageInfo.totalBytes - mStorageInfo.usedBytes; + String sizeString = Formatter.formatFileSize(mContext, freeBytes); + String label = mContext.getString(R.string.free_space_format, sizeString); + mStorageLabel.setText(label); + mStorageBar.setVisibility(GLView.VISIBLE); + mStorageLabel.setVisibility(GLView.VISIBLE); + requestLayout(); // because the size of the label may have changed. + } + + public void increaseTargetCacheSize(long delta) { + mUserChangeDelta += delta; + refreshStorageInfo(); + } + + @Override + protected void renderBackground(GLCanvas canvas) { + Rect paddings = mBackground.getPaddings(); + mBackground.draw(canvas, 0, -paddings.top, getWidth(), mTotalHeight); + } + + public void resume() { + mStorageInfoFuture = mActivity.getThreadPool().submit( + new StorageInfoJob(), + new FutureListener<StorageInfo>() { + public void onFutureDone(Future<StorageInfo> future) { + mStorageInfoFuture = null; + if (!future.isCancelled()) { + mHandler.sendMessage(mHandler.obtainMessage( + MSG_REFRESH_STORAGE, future.get())); + } + } + }); + } + + public void pause() { + if (mStorageInfoFuture != null) { + mStorageInfoFuture.cancel(); + mStorageInfoFuture = null; + } + mStorageBar.setVisibility(GLView.INVISIBLE); + mStorageLabel.setVisibility(GLView.INVISIBLE); + } + + public static class StorageInfo { + long totalBytes; // number of bytes the storage has. + long usedBytes; // number of bytes already used. + long usedCacheBytes; // number of bytes used for the cache (should be less + // then usedBytes). + long targetCacheBytes;// number of bytes used for the cache + // if all pending downloads (and removals) are completed. + } + + private class StorageInfoJob implements Job<StorageInfo> { + public StorageInfo run(JobContext jc) { + File cacheDir = mContext.getExternalCacheDir(); + if (cacheDir == null) { + cacheDir = mContext.getCacheDir(); + } + String path = cacheDir.getAbsolutePath(); + StatFs stat = new StatFs(path); + long blockSize = stat.getBlockSize(); + long availableBlocks = stat.getAvailableBlocks(); + long totalBlocks = stat.getBlockCount(); + StorageInfo si = new StorageInfo(); + si.totalBytes = blockSize * totalBlocks; + si.usedBytes = blockSize * (totalBlocks - availableBlocks); + si.usedCacheBytes = mActivity.getDataManager().getTotalUsedCacheSize(); + si.targetCacheBytes = mActivity.getDataManager().getTotalTargetCacheSize(); + return si; + } + } +} diff --git a/src/com/android/gallery3d/ui/CanvasTexture.java b/src/com/android/gallery3d/ui/CanvasTexture.java new file mode 100644 index 000000000..679a4bcdc --- /dev/null +++ b/src/com/android/gallery3d/ui/CanvasTexture.java @@ -0,0 +1,52 @@ +/* + * 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.ui; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Bitmap.Config; + +// CanvasTexture is a texture whose content is the drawing on a Canvas. +// The subclasses should override onDraw() to draw on the bitmap. +// By default CanvasTexture is not opaque. +abstract class CanvasTexture extends UploadedTexture { + protected Canvas mCanvas; + private final Config mConfig; + + public CanvasTexture(int width, int height) { + mConfig = Config.ARGB_8888; + setSize(width, height); + setOpaque(false); + } + + @Override + protected Bitmap onGetBitmap() { + Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, mConfig); + mCanvas = new Canvas(bitmap); + onDraw(mCanvas, bitmap); + return bitmap; + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + if (!inFinalizer()) { + bitmap.recycle(); + } + } + + abstract protected void onDraw(Canvas canvas, Bitmap backing); +} diff --git a/src/com/android/gallery3d/ui/ColorTexture.java b/src/com/android/gallery3d/ui/ColorTexture.java new file mode 100644 index 000000000..24e8914b5 --- /dev/null +++ b/src/com/android/gallery3d/ui/ColorTexture.java @@ -0,0 +1,58 @@ +/* + * 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.ui; + +import com.android.gallery3d.common.Utils; + +// ColorTexture is a texture which fills the rectangle with the specified color. +public class ColorTexture implements Texture { + + private final int mColor; + private int mWidth; + private int mHeight; + + public ColorTexture(int color) { + mColor = color; + mWidth = 1; + mHeight = 1; + } + + public void draw(GLCanvas canvas, int x, int y) { + draw(canvas, x, y, mWidth, mHeight); + } + + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + canvas.fillRect(x, y, w, h, mColor); + } + + public boolean isOpaque() { + return Utils.isOpaque(mColor); + } + + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } +} diff --git a/src/com/android/gallery3d/ui/Config.java b/src/com/android/gallery3d/ui/Config.java new file mode 100644 index 000000000..5c5b6210a --- /dev/null +++ b/src/com/android/gallery3d/ui/Config.java @@ -0,0 +1,31 @@ +/* + * 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.ui; + +interface DetailsWindowConfig { + public static final int FONT_SIZE = 18; + public static final int PREFERRED_WIDTH = 400; + public static final int LEFT_RIGHT_EXTRA_PADDING = 9; + public static final int TOP_BOTTOM_EXTRA_PADDING = 9; + public static final int LINE_SPACING = 5; + public static final int FIRST_LINE_SPACING = 18; +} + +interface TextButtonConfig { + public static final int HORIZONTAL_PADDINGS = 16; + public static final int VERTICAL_PADDINGS = 5; +} diff --git a/src/com/android/gallery3d/ui/CropView.java b/src/com/android/gallery3d/ui/CropView.java new file mode 100644 index 000000000..9c59c9a84 --- /dev/null +++ b/src/com/android/gallery3d/ui/CropView.java @@ -0,0 +1,801 @@ +/* + * 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.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.anim.Animation; +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.common.Utils; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.RectF; +import android.media.FaceDetector; +import android.os.Handler; +import android.os.Message; +import android.view.MotionEvent; +import android.view.animation.DecelerateInterpolator; +import android.widget.Toast; + +import java.util.ArrayList; +import javax.microedition.khronos.opengles.GL11; + +/** + * The activity can crop specific region of interest from an image. + */ +public class CropView extends GLView { + private static final String TAG = "CropView"; + + private static final int FACE_PIXEL_COUNT = 120000; // around 400x300 + + private static final int COLOR_OUTLINE = 0xFF008AFF; + private static final int COLOR_FACE_OUTLINE = 0xFF000000; + + private static final float OUTLINE_WIDTH = 3f; + + private static final int SIZE_UNKNOWN = -1; + private static final int TOUCH_TOLERANCE = 30; + + private static final float MIN_SELECTION_LENGTH = 16f; + public static final float UNSPECIFIED = -1f; + + private static final int MAX_FACE_COUNT = 3; + private static final float FACE_EYE_RATIO = 2f; + + private static final int ANIMATION_DURATION = 1250; + + private static final int MOVE_LEFT = 1; + private static final int MOVE_TOP = 2; + private static final int MOVE_RIGHT = 4; + private static final int MOVE_BOTTOM = 8; + private static final int MOVE_BLOCK = 16; + + private static final float MAX_SELECTION_RATIO = 0.8f; + private static final float MIN_SELECTION_RATIO = 0.4f; + private static final float SELECTION_RATIO = 0.60f; + private static final int ANIMATION_TRIGGER = 64; + + private static final int MSG_UPDATE_FACES = 1; + + private float mAspectRatio = UNSPECIFIED; + private float mSpotlightRatioX = 0; + private float mSpotlightRatioY = 0; + + private Handler mMainHandler; + + private FaceHighlightView mFaceDetectionView; + private HighlightRectangle mHighlightRectangle; + private TileImageView mImageView; + private AnimationController mAnimation = new AnimationController(); + + private int mImageWidth = SIZE_UNKNOWN; + private int mImageHeight = SIZE_UNKNOWN; + + private GalleryActivity mActivity; + + private GLPaint mPaint = new GLPaint(); + private GLPaint mFacePaint = new GLPaint(); + + private int mImageRotation; + + public CropView(GalleryActivity activity) { + mActivity = activity; + mImageView = new TileImageView(activity); + mFaceDetectionView = new FaceHighlightView(); + mHighlightRectangle = new HighlightRectangle(); + + addComponent(mImageView); + addComponent(mFaceDetectionView); + addComponent(mHighlightRectangle); + + mHighlightRectangle.setVisibility(GLView.INVISIBLE); + + mPaint.setColor(COLOR_OUTLINE); + mPaint.setLineWidth(OUTLINE_WIDTH); + + mFacePaint.setColor(COLOR_FACE_OUTLINE); + mFacePaint.setLineWidth(OUTLINE_WIDTH); + + mMainHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + Utils.assertTrue(message.what == MSG_UPDATE_FACES); + ((DetectFaceTask) message.obj).updateFaces(); + } + }; + } + + public void setAspectRatio(float ratio) { + mAspectRatio = ratio; + } + + public void setSpotlightRatio(float ratioX, float ratioY) { + mSpotlightRatioX = ratioX; + mSpotlightRatioY = ratioY; + } + + @Override + public void onLayout(boolean changed, int l, int t, int r, int b) { + int width = r - l; + int height = b - t; + + mFaceDetectionView.layout(0, 0, width, height); + mHighlightRectangle.layout(0, 0, width, height); + mImageView.layout(0, 0, width, height); + if (mImageHeight != SIZE_UNKNOWN) { + mAnimation.initialize(); + if (mHighlightRectangle.getVisibility() == GLView.VISIBLE) { + mAnimation.parkNow( + mHighlightRectangle.mHighlightRect); + } + } + } + + private boolean setImageViewPosition(int centerX, int centerY, float scale) { + int inverseX = mImageWidth - centerX; + int inverseY = mImageHeight - centerY; + TileImageView t = mImageView; + int rotation = mImageRotation; + switch (rotation) { + case 0: return t.setPosition(centerX, centerY, scale, 0); + case 90: return t.setPosition(centerY, inverseX, scale, 90); + case 180: return t.setPosition(inverseX, inverseY, scale, 180); + case 270: return t.setPosition(inverseY, centerX, scale, 270); + default: throw new IllegalArgumentException(String.valueOf(rotation)); + } + } + + @Override + public void render(GLCanvas canvas) { + AnimationController a = mAnimation; + if (a.calculate(canvas.currentAnimationTimeMillis())) invalidate(); + setImageViewPosition(a.getCenterX(), a.getCenterY(), a.getScale()); + super.render(canvas); + } + + @Override + public void renderBackground(GLCanvas canvas) { + canvas.clearBuffer(); + } + + public RectF getCropRectangle() { + if (mHighlightRectangle.getVisibility() == GLView.INVISIBLE) return null; + RectF rect = mHighlightRectangle.mHighlightRect; + RectF result = new RectF(rect.left * mImageWidth, rect.top * mImageHeight, + rect.right * mImageWidth, rect.bottom * mImageHeight); + return result; + } + + public int getImageWidth() { + return mImageWidth; + } + + public int getImageHeight() { + return mImageHeight; + } + + private class FaceHighlightView extends GLView { + private static final int INDEX_NONE = -1; + private ArrayList<RectF> mFaces = new ArrayList<RectF>(); + private RectF mRect = new RectF(); + private int mPressedFaceIndex = INDEX_NONE; + + public void addFace(RectF faceRect) { + mFaces.add(faceRect); + invalidate(); + } + + private void renderFace(GLCanvas canvas, RectF face, boolean pressed) { + GL11 gl = canvas.getGLInstance(); + if (pressed) { + gl.glEnable(GL11.GL_STENCIL_TEST); + gl.glClear(GL11.GL_STENCIL_BUFFER_BIT); + gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_REPLACE); + gl.glStencilFunc(GL11.GL_ALWAYS, 1, 1); + } + + RectF r = mAnimation.mapRect(face, mRect); + canvas.fillRect(r.left, r.top, r.width(), r.height(), Color.TRANSPARENT); + canvas.drawRect(r.left, r.top, r.width(), r.height(), mFacePaint); + + if (pressed) { + gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_KEEP); + } + } + + @Override + protected void renderBackground(GLCanvas canvas) { + ArrayList<RectF> faces = mFaces; + for (int i = 0, n = faces.size(); i < n; ++i) { + renderFace(canvas, faces.get(i), i == mPressedFaceIndex); + } + + GL11 gl = canvas.getGLInstance(); + if (mPressedFaceIndex != INDEX_NONE) { + gl.glStencilFunc(GL11.GL_NOTEQUAL, 1, 1); + canvas.fillRect(0, 0, getWidth(), getHeight(), 0x66000000); + gl.glDisable(GL11.GL_STENCIL_TEST); + } + } + + private void setPressedFace(int index) { + if (mPressedFaceIndex == index) return; + mPressedFaceIndex = index; + invalidate(); + } + + private int getFaceIndexByPosition(float x, float y) { + ArrayList<RectF> faces = mFaces; + for (int i = 0, n = faces.size(); i < n; ++i) { + RectF r = mAnimation.mapRect(faces.get(i), mRect); + if (r.contains(x, y)) return i; + } + return INDEX_NONE; + } + + @Override + protected boolean onTouch(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: { + setPressedFace(getFaceIndexByPosition(x, y)); + break; + } + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + int index = mPressedFaceIndex; + setPressedFace(INDEX_NONE); + if (index != INDEX_NONE) { + mHighlightRectangle.setRectangle(mFaces.get(index)); + mHighlightRectangle.setVisibility(GLView.VISIBLE); + setVisibility(GLView.INVISIBLE); + } + } + } + return true; + } + } + + private class AnimationController extends Animation { + private int mCurrentX; + private int mCurrentY; + private float mCurrentScale; + private int mStartX; + private int mStartY; + private float mStartScale; + private int mTargetX; + private int mTargetY; + private float mTargetScale; + + public AnimationController() { + setDuration(ANIMATION_DURATION); + setInterpolator(new DecelerateInterpolator(4)); + } + + public void initialize() { + mCurrentX = mImageWidth / 2; + mCurrentY = mImageHeight / 2; + mCurrentScale = Math.min(2, Math.min( + (float) getWidth() / mImageWidth, + (float) getHeight() / mImageHeight)); + } + + public void startParkingAnimation(RectF highlight) { + RectF r = mAnimation.mapRect(highlight, new RectF()); + int width = getWidth(); + int height = getHeight(); + + float wr = r.width() / width; + float hr = r.height() / height; + final int d = ANIMATION_TRIGGER; + if (wr >= MIN_SELECTION_RATIO && wr < MAX_SELECTION_RATIO + && hr >= MIN_SELECTION_RATIO && hr < MAX_SELECTION_RATIO + && r.left >= d && r.right < width - d + && r.top >= d && r.bottom < height - d) return; + + mStartX = mCurrentX; + mStartY = mCurrentY; + mStartScale = mCurrentScale; + calculateTarget(highlight); + start(); + } + + public void parkNow(RectF highlight) { + calculateTarget(highlight); + forceStop(); + mStartX = mCurrentX = mTargetX; + mStartY = mCurrentY = mTargetY; + mStartScale = mCurrentScale = mTargetScale; + } + + public void inverseMapPoint(PointF point) { + float s = mCurrentScale; + point.x = Utils.clamp(((point.x - getWidth() * 0.5f) / s + + mCurrentX) / mImageWidth, 0, 1); + point.y = Utils.clamp(((point.y - getHeight() * 0.5f) / s + + mCurrentY) / mImageHeight, 0, 1); + } + + public RectF mapRect(RectF input, RectF output) { + float offsetX = getWidth() * 0.5f; + float offsetY = getHeight() * 0.5f; + int x = mCurrentX; + int y = mCurrentY; + float s = mCurrentScale; + output.set( + offsetX + (input.left * mImageWidth - x) * s, + offsetY + (input.top * mImageHeight - y) * s, + offsetX + (input.right * mImageWidth - x) * s, + offsetY + (input.bottom * mImageHeight - y) * s); + return output; + } + + @Override + protected void onCalculate(float progress) { + mCurrentX = Math.round(mStartX + (mTargetX - mStartX) * progress); + mCurrentY = Math.round(mStartY + (mTargetY - mStartY) * progress); + mCurrentScale = mStartScale + (mTargetScale - mStartScale) * progress; + + if (mCurrentX == mTargetX && mCurrentY == mTargetY + && mCurrentScale == mTargetScale) forceStop(); + } + + public int getCenterX() { + return mCurrentX; + } + + public int getCenterY() { + return mCurrentY; + } + + public float getScale() { + return mCurrentScale; + } + + private void calculateTarget(RectF highlight) { + float width = getWidth(); + float height = getHeight(); + + if (mImageWidth != SIZE_UNKNOWN) { + float minScale = Math.min(width / mImageWidth, height / mImageHeight); + float scale = Utils.clamp(SELECTION_RATIO * Math.min( + width / (highlight.width() * mImageWidth), + height / (highlight.height() * mImageHeight)), minScale, 2f); + int centerX = Math.round( + mImageWidth * (highlight.left + highlight.right) * 0.5f); + int centerY = Math.round( + mImageHeight * (highlight.top + highlight.bottom) * 0.5f); + + if (Math.round(mImageWidth * scale) > width) { + int limitX = Math.round(width * 0.5f / scale); + centerX = Math.round( + (highlight.left + highlight.right) * mImageWidth / 2); + centerX = Utils.clamp(centerX, limitX, mImageWidth - limitX); + } else { + centerX = mImageWidth / 2; + } + if (Math.round(mImageHeight * scale) > height) { + int limitY = Math.round(height * 0.5f / scale); + centerY = Math.round( + (highlight.top + highlight.bottom) * mImageHeight / 2); + centerY = Utils.clamp(centerY, limitY, mImageHeight - limitY); + } else { + centerY = mImageHeight / 2; + } + mTargetX = centerX; + mTargetY = centerY; + mTargetScale = scale; + } + } + + } + + private class HighlightRectangle extends GLView { + private RectF mHighlightRect = new RectF(0.25f, 0.25f, 0.75f, 0.75f); + private RectF mTempRect = new RectF(); + private PointF mTempPoint = new PointF(); + + private ResourceTexture mArrowX; + private ResourceTexture mArrowY; + + private int mMovingEdges = 0; + private float mReferenceX; + private float mReferenceY; + + public HighlightRectangle() { + mArrowX = new ResourceTexture(mActivity.getAndroidContext(), + R.drawable.camera_crop_width_holo); + mArrowY = new ResourceTexture(mActivity.getAndroidContext(), + R.drawable.camera_crop_height_holo); + } + + public void setInitRectangle() { + float targetRatio = mAspectRatio == UNSPECIFIED + ? 1f + : mAspectRatio * mImageHeight / mImageWidth; + float w = SELECTION_RATIO / 2f; + float h = SELECTION_RATIO / 2f; + if (targetRatio > 1) { + h = w / targetRatio; + } else { + w = h * targetRatio; + } + mHighlightRect.set(0.5f - w, 0.5f - h, 0.5f + w, 0.5f + h); + } + + public void setRectangle(RectF faceRect) { + mHighlightRect.set(faceRect); + mAnimation.startParkingAnimation(faceRect); + invalidate(); + } + + private void moveEdges(MotionEvent event) { + float scale = mAnimation.getScale(); + float dx = (event.getX() - mReferenceX) / scale / mImageWidth; + float dy = (event.getY() - mReferenceY) / scale / mImageHeight; + mReferenceX = event.getX(); + mReferenceY = event.getY(); + RectF r = mHighlightRect; + + if ((mMovingEdges & MOVE_BLOCK) != 0) { + dx = Utils.clamp(dx, -r.left, 1 - r.right); + dy = Utils.clamp(dy, -r.top , 1 - r.bottom); + r.top += dy; + r.bottom += dy; + r.left += dx; + r.right += dx; + } else { + PointF point = mTempPoint; + point.set(mReferenceX, mReferenceY); + mAnimation.inverseMapPoint(point); + float left = r.left + MIN_SELECTION_LENGTH / mImageWidth; + float right = r.right - MIN_SELECTION_LENGTH / mImageWidth; + float top = r.top + MIN_SELECTION_LENGTH / mImageHeight; + float bottom = r.bottom - MIN_SELECTION_LENGTH / mImageHeight; + if ((mMovingEdges & MOVE_RIGHT) != 0) { + r.right = Utils.clamp(point.x, left, 1f); + } + if ((mMovingEdges & MOVE_LEFT) != 0) { + r.left = Utils.clamp(point.x, 0, right); + } + if ((mMovingEdges & MOVE_TOP) != 0) { + r.top = Utils.clamp(point.y, 0, bottom); + } + if ((mMovingEdges & MOVE_BOTTOM) != 0) { + r.bottom = Utils.clamp(point.y, top, 1f); + } + if (mAspectRatio != UNSPECIFIED) { + float targetRatio = mAspectRatio * mImageHeight / mImageWidth; + if (r.width() / r.height() > targetRatio) { + float height = r.width() / targetRatio; + if ((mMovingEdges & MOVE_BOTTOM) != 0) { + r.bottom = Utils.clamp(r.top + height, top, 1f); + } else { + r.top = Utils.clamp(r.bottom - height, 0, bottom); + } + } else { + float width = r.height() * targetRatio; + if ((mMovingEdges & MOVE_LEFT) != 0) { + r.left = Utils.clamp(r.right - width, 0, right); + } else { + r.right = Utils.clamp(r.left + width, left, 1f); + } + } + if (r.width() / r.height() > targetRatio) { + float width = r.height() * targetRatio; + if ((mMovingEdges & MOVE_LEFT) != 0) { + r.left = Utils.clamp(r.right - width, 0, right); + } else { + r.right = Utils.clamp(r.left + width, left, 1f); + } + } else { + float height = r.width() / targetRatio; + if ((mMovingEdges & MOVE_BOTTOM) != 0) { + r.bottom = Utils.clamp(r.top + height, top, 1f); + } else { + r.top = Utils.clamp(r.bottom - height, 0, bottom); + } + } + } + } + invalidate(); + } + + private void setMovingEdges(MotionEvent event) { + RectF r = mAnimation.mapRect(mHighlightRect, mTempRect); + float x = event.getX(); + float y = event.getY(); + + if (x > r.left + TOUCH_TOLERANCE && x < r.right - TOUCH_TOLERANCE + && y > r.top + TOUCH_TOLERANCE && y < r.bottom - TOUCH_TOLERANCE) { + mMovingEdges = MOVE_BLOCK; + return; + } + + boolean inVerticalRange = (r.top - TOUCH_TOLERANCE) <= y + && y <= (r.bottom + TOUCH_TOLERANCE); + boolean inHorizontalRange = (r.left - TOUCH_TOLERANCE) <= x + && x <= (r.right + TOUCH_TOLERANCE); + + if (inVerticalRange) { + boolean left = Math.abs(x - r.left) <= TOUCH_TOLERANCE; + boolean right = Math.abs(x - r.right) <= TOUCH_TOLERANCE; + if (left && right) { + left = Math.abs(x - r.left) < Math.abs(x - r.right); + right = !left; + } + if (left) mMovingEdges |= MOVE_LEFT; + if (right) mMovingEdges |= MOVE_RIGHT; + if (mAspectRatio != UNSPECIFIED && inHorizontalRange) { + mMovingEdges |= (y > + (r.top + r.bottom) / 2) ? MOVE_BOTTOM : MOVE_TOP; + } + } + if (inHorizontalRange) { + boolean top = Math.abs(y - r.top) <= TOUCH_TOLERANCE; + boolean bottom = Math.abs(y - r.bottom) <= TOUCH_TOLERANCE; + if (top && bottom) { + top = Math.abs(y - r.top) < Math.abs(y - r.bottom); + bottom = !top; + } + if (top) mMovingEdges |= MOVE_TOP; + if (bottom) mMovingEdges |= MOVE_BOTTOM; + if (mAspectRatio != UNSPECIFIED && inVerticalRange) { + mMovingEdges |= (x > + (r.left + r.right) / 2) ? MOVE_RIGHT : MOVE_LEFT; + } + } + } + + @Override + protected boolean onTouch(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + mReferenceX = event.getX(); + mReferenceY = event.getY(); + setMovingEdges(event); + invalidate(); + return true; + } + case MotionEvent.ACTION_MOVE: + moveEdges(event); + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + mMovingEdges = 0; + mAnimation.startParkingAnimation(mHighlightRect); + invalidate(); + return true; + } + } + return true; + } + + @Override + protected void renderBackground(GLCanvas canvas) { + RectF r = mAnimation.mapRect(mHighlightRect, mTempRect); + drawHighlightRectangle(canvas, r); + + float centerY = (r.top + r.bottom) / 2; + float centerX = (r.left + r.right) / 2; + if ((mMovingEdges & (MOVE_RIGHT | MOVE_BLOCK)) != 0) { + mArrowX.draw(canvas, + Math.round(r.right - mArrowX.getWidth() / 2), + Math.round(centerY - mArrowX.getHeight() / 2)); + } + if ((mMovingEdges & (MOVE_LEFT | MOVE_BLOCK)) != 0) { + mArrowX.draw(canvas, + Math.round(r.left - mArrowX.getWidth() / 2), + Math.round(centerY - mArrowX.getHeight() / 2)); + } + if ((mMovingEdges & (MOVE_TOP | MOVE_BLOCK)) != 0) { + mArrowY.draw(canvas, + Math.round(centerX - mArrowY.getWidth() / 2), + Math.round(r.top - mArrowY.getHeight() / 2)); + } + if ((mMovingEdges & (MOVE_BOTTOM | MOVE_BLOCK)) != 0) { + mArrowY.draw(canvas, + Math.round(centerX - mArrowY.getWidth() / 2), + Math.round(r.bottom - mArrowY.getHeight() / 2)); + } + } + + private void drawHighlightRectangle(GLCanvas canvas, RectF r) { + GL11 gl = canvas.getGLInstance(); + gl.glLineWidth(3.0f); + gl.glEnable(GL11.GL_LINE_SMOOTH); + + gl.glEnable(GL11.GL_STENCIL_TEST); + gl.glClear(GL11.GL_STENCIL_BUFFER_BIT); + gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_REPLACE); + gl.glStencilFunc(GL11.GL_ALWAYS, 1, 1); + + if (mSpotlightRatioX == 0 || mSpotlightRatioY == 0) { + canvas.fillRect(r.left, r.top, r.width(), r.height(), Color.TRANSPARENT); + canvas.drawRect(r.left, r.top, r.width(), r.height(), mPaint); + } else { + float sx = r.width() * mSpotlightRatioX; + float sy = r.height() * mSpotlightRatioY; + float cx = r.centerX(); + float cy = r.centerY(); + + canvas.fillRect(cx - sx / 2, cy - sy / 2, sx, sy, Color.TRANSPARENT); + canvas.drawRect(cx - sx / 2, cy - sy / 2, sx, sy, mPaint); + canvas.drawRect(r.left, r.top, r.width(), r.height(), mPaint); + + gl.glStencilFunc(GL11.GL_NOTEQUAL, 1, 1); + gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_REPLACE); + + canvas.drawRect(cx - sy / 2, cy - sx / 2, sy, sx, mPaint); + canvas.fillRect(cx - sy / 2, cy - sx / 2, sy, sx, Color.TRANSPARENT); + canvas.fillRect(r.left, r.top, r.width(), r.height(), 0x80000000); + } + + gl.glStencilFunc(GL11.GL_NOTEQUAL, 1, 1); + gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_KEEP); + + canvas.fillRect(0, 0, getWidth(), getHeight(), 0xA0000000); + + gl.glDisable(GL11.GL_STENCIL_TEST); + } + } + + private class DetectFaceTask extends Thread { + private final FaceDetector.Face[] mFaces = new FaceDetector.Face[MAX_FACE_COUNT]; + private final Bitmap mFaceBitmap; + private int mFaceCount; + + public DetectFaceTask(Bitmap bitmap) { + mFaceBitmap = bitmap; + setName("face-detect"); + } + + @Override + public void run() { + Bitmap bitmap = mFaceBitmap; + FaceDetector detector = new FaceDetector( + bitmap.getWidth(), bitmap.getHeight(), MAX_FACE_COUNT); + mFaceCount = detector.findFaces(bitmap, mFaces); + mMainHandler.sendMessage( + mMainHandler.obtainMessage(MSG_UPDATE_FACES, this)); + } + + private RectF getFaceRect(FaceDetector.Face face) { + PointF point = new PointF(); + face.getMidPoint(point); + + int width = mFaceBitmap.getWidth(); + int height = mFaceBitmap.getHeight(); + float rx = face.eyesDistance() * FACE_EYE_RATIO; + float ry = rx; + float aspect = mAspectRatio; + if (aspect != UNSPECIFIED) { + if (aspect > 1) { + rx = ry * aspect; + } else { + ry = rx / aspect; + } + } + + RectF r = new RectF( + point.x - rx, point.y - ry, point.x + rx, point.y + ry); + r.intersect(0, 0, width, height); + + if (aspect != UNSPECIFIED) { + if (r.width() / r.height() > aspect) { + float w = r.height() * aspect; + r.left = (r.left + r.right - w) * 0.5f; + r.right = r.left + w; + } else { + float h = r.width() / aspect; + r.top = (r.top + r.bottom - h) * 0.5f; + r.bottom = r.top + h; + } + } + + r.left /= width; + r.right /= width; + r.top /= height; + r.bottom /= height; + return r; + } + + public void updateFaces() { + if (mFaceCount > 1) { + for (int i = 0, n = mFaceCount; i < n; ++i) { + mFaceDetectionView.addFace(getFaceRect(mFaces[i])); + } + mFaceDetectionView.setVisibility(GLView.VISIBLE); + Toast.makeText(mActivity.getAndroidContext(), + R.string.multiface_crop_help, Toast.LENGTH_SHORT).show(); + } else if (mFaceCount == 1) { + mFaceDetectionView.setVisibility(GLView.INVISIBLE); + mHighlightRectangle.setRectangle(getFaceRect(mFaces[0])); + mHighlightRectangle.setVisibility(GLView.VISIBLE); + } else /*mFaceCount == 0*/ { + mHighlightRectangle.setInitRectangle(); + mHighlightRectangle.setVisibility(GLView.VISIBLE); + } + } + } + + public void setDataModel(TileImageView.Model dataModel, int rotation) { + if (((rotation / 90) & 0x01) != 0) { + mImageWidth = dataModel.getImageHeight(); + mImageHeight = dataModel.getImageWidth(); + } else { + mImageWidth = dataModel.getImageWidth(); + mImageHeight = dataModel.getImageHeight(); + } + + mImageRotation = rotation; + + mImageView.setModel(dataModel); + mAnimation.initialize(); + } + + public void detectFaces(Bitmap bitmap) { + int rotation = mImageRotation; + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + float scale = (float) Math.sqrt( + (double) FACE_PIXEL_COUNT / (width * height)); + + // faceBitmap is a correctly rotated bitmap, as viewed by a user. + Bitmap faceBitmap; + if (((rotation / 90) & 1) == 0) { + int w = (Math.round(width * scale) & ~1); // must be even + int h = Math.round(height * scale); + faceBitmap = Bitmap.createBitmap(w, h, Config.RGB_565); + Canvas canvas = new Canvas(faceBitmap); + canvas.rotate(rotation, w / 2, h / 2); + canvas.scale((float) w / width, (float) h / height); + canvas.drawBitmap(bitmap, 0, 0, new Paint(Paint.FILTER_BITMAP_FLAG)); + } else { + int w = (Math.round(height * scale) & ~1); // must be even + int h = Math.round(width * scale); + faceBitmap = Bitmap.createBitmap(w, h, Config.RGB_565); + Canvas canvas = new Canvas(faceBitmap); + canvas.translate(w / 2, h / 2); + canvas.rotate(rotation); + canvas.translate(-h / 2, -w / 2); + canvas.scale((float) w / height, (float) h / width); + canvas.drawBitmap(bitmap, 0, 0, new Paint(Paint.FILTER_BITMAP_FLAG)); + } + new DetectFaceTask(faceBitmap).start(); + } + + public void initializeHighlightRectangle() { + mHighlightRectangle.setInitRectangle(); + mHighlightRectangle.setVisibility(GLView.VISIBLE); + } + + public void resume() { + mImageView.prepareTextures(); + } + + public void pause() { + mImageView.freeTextures(); + } +} + diff --git a/src/com/android/gallery3d/ui/CustomMenu.java b/src/com/android/gallery3d/ui/CustomMenu.java new file mode 100644 index 000000000..de2367e60 --- /dev/null +++ b/src/com/android/gallery3d/ui/CustomMenu.java @@ -0,0 +1,126 @@ +/* + * 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.ui; + +import com.android.gallery3d.R; + +import android.app.ActionBar; +import android.content.Context; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; + +import java.util.ArrayList; + +public class CustomMenu implements OnMenuItemClickListener { + @SuppressWarnings("unused") + private static final String TAG = "FilterMenu"; + + public static class DropDownMenu { + private Button mButton; + private PopupMenu mPopupMenu; + private Menu mMenu; + + public DropDownMenu(Context context, Button button, int menuId, + OnMenuItemClickListener listener) { + mButton = button; + mButton.setBackgroundDrawable(context.getResources().getDrawable( + R.drawable.dropdown_normal_holo_dark)); + mPopupMenu = new PopupMenu(context, mButton); + mMenu = mPopupMenu.getMenu(); + mPopupMenu.getMenuInflater().inflate(menuId, mMenu); + mPopupMenu.setOnMenuItemClickListener(listener); + mButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mPopupMenu.show(); + } + }); + } + + public MenuItem findItem(int id) { + return mMenu.findItem(id); + } + + public void setTitle(CharSequence title) { + mButton.setText(title); + } + } + + + + private Context mContext; + private ArrayList<DropDownMenu> mMenus; + private OnMenuItemClickListener mListener; + + public CustomMenu(Context context) { + mContext = context; + mMenus = new ArrayList<DropDownMenu>(); + } + + public DropDownMenu addDropDownMenu(Button button, int menuId) { + DropDownMenu menu = new DropDownMenu(mContext, button, menuId, this); + mMenus.add(menu); + return menu; + } + + public void setOnMenuItemClickListener(OnMenuItemClickListener listener) { + mListener = listener; + } + + public MenuItem findMenuItem(int id) { + MenuItem item = null; + for (DropDownMenu menu : mMenus) { + item = menu.findItem(id); + if (item != null) return item; + } + return item; + } + + public void setMenuItemAppliedEnabled(int id, boolean applied, boolean enabled, + boolean updateTitle) { + MenuItem item = null; + for (DropDownMenu menu : mMenus) { + item = menu.findItem(id); + if (item != null) { + item.setCheckable(true); + item.setChecked(applied); + item.setEnabled(enabled); + if (updateTitle) { + menu.setTitle(item.getTitle()); + } + } + } + } + + public void setMenuItemVisibility(int id, boolean visibility) { + MenuItem item = findMenuItem(id); + if (item != null) { + item.setVisible(visibility); + } + } + + public boolean onMenuItemClick(MenuItem item) { + if (mListener != null) { + return mListener.onMenuItemClick(item); + } + return false; + } +} diff --git a/src/com/android/gallery3d/ui/DetailsWindow.java b/src/com/android/gallery3d/ui/DetailsWindow.java new file mode 100644 index 000000000..03e216922 --- /dev/null +++ b/src/com/android/gallery3d/ui/DetailsWindow.java @@ -0,0 +1,451 @@ +/* + * 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.ui; + +import static com.android.gallery3d.ui.DetailsWindowConfig.FONT_SIZE; +import static com.android.gallery3d.ui.DetailsWindowConfig.LEFT_RIGHT_EXTRA_PADDING; +import static com.android.gallery3d.ui.DetailsWindowConfig.LINE_SPACING; +import static com.android.gallery3d.ui.DetailsWindowConfig.PREFERRED_WIDTH; +import static com.android.gallery3d.ui.DetailsWindowConfig.TOP_BOTTOM_EXTRA_PADDING; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.MediaDetails; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.ReverseGeocoder; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Rect; +import android.location.Address; +import android.os.Handler; +import android.os.Message; +import android.text.format.Formatter; +import android.view.MotionEvent; +import android.view.View.MeasureSpec; + +import java.util.ArrayList; +import java.util.Map.Entry; + +// TODO: Add scroll bar to this window. +public class DetailsWindow extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "DetailsWindow"; + private static final int MSG_REFRESH_LOCATION = 1; + private static final int FONT_COLOR = Color.WHITE; + private static final int CLOSE_BUTTON_SIZE = 32; + + private GalleryActivity mContext; + protected Texture mBackground; + private StringTexture mTitle; + private MyDataModel mModel; + private MediaDetails mDetails; + private DetailsSource mSource; + private int mIndex; + private int mLocationIndex; + private Future<Address> mAddressLookupJob; + private Handler mHandler; + private Icon mCloseButton; + private int mMaxDetailLength; + private CloseListener mListener; + + private ScrollView mScrollView; + private DetailsPanel mDetailPanel = new DetailsPanel(); + + public interface DetailsSource { + public int size(); + public int findIndex(int indexHint); + public MediaDetails getDetails(); + } + + public interface CloseListener { + public void onClose(); + } + + public DetailsWindow(GalleryActivity activity, DetailsSource source) { + mContext = activity; + mSource = source; + mHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + public void handleMessage(Message msg) { + switch(msg.what) { + case MSG_REFRESH_LOCATION: + mModel.updateLocation((Address) msg.obj); + invalidate(); + break; + } + } + }; + Context context = activity.getAndroidContext(); + ResourceTexture icon = new ResourceTexture(context, R.drawable.ic_menu_cancel_holo_light); + setBackground(new NinePatchTexture(context, R.drawable.popup_full_dark)); + + mCloseButton = new Icon(context, icon, CLOSE_BUTTON_SIZE, CLOSE_BUTTON_SIZE) { + @Override + protected boolean onTouch(MotionEvent event) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_UP: + if (mListener != null) mListener.onClose(); + } + return true; + } + }; + mScrollView = new ScrollView(context); + mScrollView.addComponent(mDetailPanel); + + super.addComponent(mScrollView); + super.addComponent(mCloseButton); + + reloadDetails(0); + } + + public void setCloseListener(CloseListener listener) { + mListener = listener; + } + + public void setBackground(Texture background) { + if (background == mBackground) return; + mBackground = background; + if (background != null && background instanceof NinePatchTexture) { + Rect p = ((NinePatchTexture) mBackground).getPaddings(); + p.left += LEFT_RIGHT_EXTRA_PADDING; + p.right += LEFT_RIGHT_EXTRA_PADDING; + p.top += TOP_BOTTOM_EXTRA_PADDING; + p.bottom += TOP_BOTTOM_EXTRA_PADDING; + setPaddings(p); + } else { + setPaddings(0, 0, 0, 0); + } + Rect p = getPaddings(); + mMaxDetailLength = PREFERRED_WIDTH - p.left - p.right; + invalidate(); + } + + public void setTitle(String title) { + mTitle = StringTexture.newInstance(title, FONT_SIZE, FONT_COLOR); + } + + @Override + protected void renderBackground(GLCanvas canvas) { + if (mBackground == null) return; + int width = getWidth(); + int height = getHeight(); + + //TODO: change alpha in the background image. + canvas.save(GLCanvas.SAVE_FLAG_ALPHA); + canvas.setAlpha(0.7f); + mBackground.draw(canvas, 0, 0, width, height); + canvas.restore(); + + Rect p = getPaddings(); + if (mTitle != null) mTitle.draw(canvas, p.left, p.top); + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + int height = MeasureSpec.getSize(heightSpec); + MeasureHelper.getInstance(this) + .setPreferredContentSize(PREFERRED_WIDTH, height) + .measure(widthSpec, heightSpec); + } + + @Override + protected void onLayout(boolean sizeChange, int l, int t, int r, int b) { + mCloseButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + int bWidth = mCloseButton.getMeasuredWidth(); + int bHeight = mCloseButton.getMeasuredHeight(); + int width = getWidth(); + int height = getHeight(); + + Rect p = getPaddings(); + mCloseButton.layout(width - p.right - bWidth, p.top, + width - p.right, p.top + bHeight); + mScrollView.layout(p.left, p.top + bHeight, width - p.right, + height - p.bottom); + } + + public void show() { + setVisibility(GLView.VISIBLE); + requestLayout(); + } + + public void hide() { + setVisibility(GLView.INVISIBLE); + requestLayout(); + } + + public void pause() { + Future<Address> lookupJob = mAddressLookupJob; + if (lookupJob != null) { + lookupJob.cancel(); + lookupJob.waitDone(); + } + } + + public void reloadDetails(int indexHint) { + int index = mSource.findIndex(indexHint); + if (index == -1) return; + MediaDetails details = mSource.getDetails(); + if (details != null) { + if (mIndex == index && mDetails == details) return; + mIndex = index; + mDetails = details; + setDetails(details); + } + mDetailPanel.requestLayout(); + } + + private void setDetails(MediaDetails details) { + mModel = new MyDataModel(details); + invalidate(); + } + + private class AddressLookupJob implements Job<Address> { + double[] mLatlng; + protected AddressLookupJob(double[] latlng) { + mLatlng = latlng; + } + + public Address run(JobContext jc) { + ReverseGeocoder geocoder = new ReverseGeocoder(mContext.getAndroidContext()); + return geocoder.lookupAddress(mLatlng[0], mLatlng[1], true); + } + } + + private class MyDataModel { + ArrayList<Texture> mItems; + + public MyDataModel(MediaDetails details) { + Context context = mContext.getAndroidContext(); + mLocationIndex = -1; + mItems = new ArrayList<Texture>(details.size()); + setTitle(String.format(context.getString(R.string.sequence_in_set), + mIndex + 1, mSource.size())); + setDetails(context, details); + } + + private void setDetails(Context context, MediaDetails details) { + for (Entry<Integer, Object> detail : details) { + String value; + switch (detail.getKey()) { + case MediaDetails.INDEX_LOCATION: { + value = getLocationText((double[]) detail.getValue()); + break; + } + case MediaDetails.INDEX_SIZE: { + value = Formatter.formatFileSize( + context, (Long) detail.getValue()); + break; + } + case MediaDetails.INDEX_WHITE_BALANCE: { + value = "1".equals(detail.getValue()) + ? context.getString(R.string.manual) + : context.getString(R.string.auto); + break; + } + case MediaDetails.INDEX_FLASH: { + MediaDetails.FlashState flash = + (MediaDetails.FlashState) detail.getValue(); + // TODO: camera doesn't fill in the complete values, show more information + // when it is fixed. + if (flash.isFlashFired()) { + value = context.getString(R.string.flash_on); + } else { + value = context.getString(R.string.flash_off); + } + break; + } + case MediaDetails.INDEX_EXPOSURE_TIME: { + value = (String) detail.getValue(); + double time = Double.valueOf(value); + if (time < 1.0f) { + value = String.format("1/%d", (int) (0.5f + 1 / time)); + } else { + int integer = (int) time; + time -= integer; + value = String.valueOf(integer) + "''"; + if (time > 0.0001) { + value += String.format(" 1/%d", (int) (0.5f + 1 / time)); + } + } + break; + } + default: { + Object valueObj = detail.getValue(); + // This shouldn't happen, log its key to help us diagnose the problem. + Utils.assertTrue(valueObj != null, "%s's value is Null", + getName(context, detail.getKey())); + value = valueObj.toString(); + } + } + int key = detail.getKey(); + if (details.hasUnit(key)) { + value = String.format("%s : %s %s", getName(context, key), value, + context.getString(details.getUnit(key))); + } else { + value = String.format("%s : %s", getName(context, key), value); + } + Texture label = MultiLineTexture.newInstance( + value, mMaxDetailLength, FONT_SIZE, FONT_COLOR); + mItems.add(label); + } + } + + private String getLocationText(double[] latlng) { + String text = String.format("(%f, %f)", latlng[0], latlng[1]); + mAddressLookupJob = mContext.getThreadPool().submit( + new AddressLookupJob(latlng), + new FutureListener<Address>() { + public void onFutureDone(Future<Address> future) { + mAddressLookupJob = null; + if (!future.isCancelled()) { + mHandler.sendMessage(mHandler.obtainMessage( + MSG_REFRESH_LOCATION, future.get())); + } + } + }); + mLocationIndex = mItems.size(); + return text; + } + + public void updateLocation(Address address) { + int index = mLocationIndex; + if (address != null && index >=0 && index < mItems.size()) { + Context context = mContext.getAndroidContext(); + String parts[] = { + address.getAdminArea(), + address.getSubAdminArea(), + address.getLocality(), + address.getSubLocality(), + address.getThoroughfare(), + address.getSubThoroughfare(), + address.getPremises(), + address.getPostalCode(), + address.getCountryName() + }; + + String addressText = ""; + for (int i = 0; i < parts.length; i++) { + if (parts[i] == null || parts[i].isEmpty()) continue; + if (!addressText.isEmpty()) { + addressText += ", "; + } + addressText += parts[i]; + } + String text = String.format("%s : %s", getName(context, + MediaDetails.INDEX_LOCATION), addressText); + mItems.set(index, MultiLineTexture.newInstance( + text, mMaxDetailLength, FONT_SIZE, FONT_COLOR)); + } + } + + public Texture getView(int index) { + return mItems.get(index); + } + + public int size() { + return mItems.size(); + } + } + + private static String getName(Context context, int key) { + switch (key) { + case MediaDetails.INDEX_TITLE: + return context.getString(R.string.title); + case MediaDetails.INDEX_DESCRIPTION: + return context.getString(R.string.description); + case MediaDetails.INDEX_DATETIME: + return context.getString(R.string.time); + case MediaDetails.INDEX_LOCATION: + return context.getString(R.string.location); + case MediaDetails.INDEX_PATH: + return context.getString(R.string.path); + case MediaDetails.INDEX_WIDTH: + return context.getString(R.string.width); + case MediaDetails.INDEX_HEIGHT: + return context.getString(R.string.height); + case MediaDetails.INDEX_ORIENTATION: + return context.getString(R.string.orientation); + case MediaDetails.INDEX_DURATION: + return context.getString(R.string.duration); + case MediaDetails.INDEX_MIMETYPE: + return context.getString(R.string.mimetype); + case MediaDetails.INDEX_SIZE: + return context.getString(R.string.file_size); + case MediaDetails.INDEX_MAKE: + return context.getString(R.string.maker); + case MediaDetails.INDEX_MODEL: + return context.getString(R.string.model); + case MediaDetails.INDEX_FLASH: + return context.getString(R.string.flash); + case MediaDetails.INDEX_APERTURE: + return context.getString(R.string.aperture); + case MediaDetails.INDEX_FOCAL_LENGTH: + return context.getString(R.string.focal_length); + case MediaDetails.INDEX_WHITE_BALANCE: + return context.getString(R.string.white_balance); + case MediaDetails.INDEX_EXPOSURE_TIME: + return context.getString(R.string.exposure_time); + case MediaDetails.INDEX_ISO: + return context.getString(R.string.iso); + default: + return "Unknown key" + key; + } + } + + private class DetailsPanel extends GLView { + + @Override + public void onMeasure(int widthSpec, int heightSpec) { + if (mTitle == null || mModel == null) { + MeasureHelper.getInstance(this) + .setPreferredContentSize(PREFERRED_WIDTH, 0) + .measure(widthSpec, heightSpec); + return; + } + + int h = getPaddings().top + LINE_SPACING; + for (int i = 0, n = mModel.size(); i < n; ++i) { + h += mModel.getView(i).getHeight() + LINE_SPACING; + } + + MeasureHelper.getInstance(this) + .setPreferredContentSize(PREFERRED_WIDTH, h) + .measure(widthSpec, heightSpec); + } + + @Override + protected void render(GLCanvas canvas) { + super.render(canvas); + + if (mTitle == null || mModel == null) { + return; + } + Rect p = getPaddings(); + int x = p.left, y = p.top + LINE_SPACING; + for (int i = 0, n = mModel.size(); i < n ; i++) { + Texture t = mModel.getView(i); + t.draw(canvas, x, y); + y += t.getHeight() + LINE_SPACING; + } + } + } +} diff --git a/src/com/android/gallery3d/ui/DisplayItem.java b/src/com/android/gallery3d/ui/DisplayItem.java new file mode 100644 index 000000000..3038232f6 --- /dev/null +++ b/src/com/android/gallery3d/ui/DisplayItem.java @@ -0,0 +1,45 @@ +/* + * 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.ui; + +public abstract class DisplayItem { + + protected int mWidth; + protected int mHeight; + + protected void setSize(int width, int height) { + mWidth = width; + mHeight = height; + } + + // returns true if more pass is needed + public abstract boolean render(GLCanvas canvas, int pass); + + public abstract long getIdentity(); + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + + public int getRotation() { + return 0; + } +} diff --git a/src/com/android/gallery3d/ui/DownUpDetector.java b/src/com/android/gallery3d/ui/DownUpDetector.java new file mode 100644 index 000000000..19db77262 --- /dev/null +++ b/src/com/android/gallery3d/ui/DownUpDetector.java @@ -0,0 +1,61 @@ +/* + * 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.ui; + +import android.view.MotionEvent; + +public class DownUpDetector { + public interface DownUpListener { + void onDown(MotionEvent e); + void onUp(MotionEvent e); + } + + private boolean mStillDown; + private DownUpListener mListener; + + public DownUpDetector(DownUpListener listener) { + mListener = listener; + } + + private void setState(boolean down, MotionEvent e) { + if (down == mStillDown) return; + mStillDown = down; + if (down) { + mListener.onDown(e); + } else { + mListener.onUp(e); + } + } + + public void onTouchEvent(MotionEvent ev) { + switch (ev.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + setState(true, ev); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_POINTER_DOWN: // Multitouch event - abort. + setState(false, ev); + break; + } + } + + public boolean isDown() { + return mStillDown; + } +} diff --git a/src/com/android/gallery3d/ui/DrawableTexture.java b/src/com/android/gallery3d/ui/DrawableTexture.java new file mode 100644 index 000000000..5c3964d5c --- /dev/null +++ b/src/com/android/gallery3d/ui/DrawableTexture.java @@ -0,0 +1,38 @@ +/* + * 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.ui; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; + +// DrawableTexture is a texture whose content is from a Drawable. +public class DrawableTexture extends CanvasTexture { + + private final Drawable mDrawable; + + public DrawableTexture(Drawable drawable, int width, int height) { + super(width, height); + mDrawable = drawable; + } + + @Override + protected void onDraw(Canvas canvas, Bitmap backing) { + mDrawable.setBounds(0, 0, mWidth, mHeight); + mDrawable.draw(canvas); + } +} diff --git a/src/com/android/gallery3d/ui/FilmStripView.java b/src/com/android/gallery3d/ui/FilmStripView.java new file mode 100644 index 000000000..8d28f2c7b --- /dev/null +++ b/src/com/android/gallery3d/ui/FilmStripView.java @@ -0,0 +1,261 @@ +/* + * 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.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.anim.AlphaAnimation; +import com.android.gallery3d.anim.CanvasAnimation; +import com.android.gallery3d.app.AlbumDataAdapter; +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.data.MediaSet; + +import android.content.Context; +import android.view.MotionEvent; +import android.view.View.MeasureSpec; + +public class FilmStripView extends GLView implements SlotView.Listener, + ScrollBarView.Listener, UserInteractionListener { + @SuppressWarnings("unused") + private static final String TAG = "FilmStripView"; + + private static final int HIDE_ANIMATION_DURATION = 300; // 0.3 sec + + public interface Listener { + void onSlotSelected(int slotIndex); + } + + private int mTopMargin, mMidMargin, mBottomMargin; + private int mContentSize, mBarSize, mGripSize; + private AlbumView mAlbumView; + private ScrollBarView mScrollBarView; + private AlbumDataAdapter mAlbumDataAdapter; + private StripDrawer mStripDrawer; + private Listener mListener; + private UserInteractionListener mUIListener; + private boolean mFilmStripVisible; + private CanvasAnimation mFilmStripAnimation; + private NinePatchTexture mBackgroundTexture; + + // The layout of FileStripView is + // topMargin + // ----+----+ + // / +----+--\ + // contentSize | | thumbSize + // \ +----+--/ + // ----+----+ + // midMargin + // ----+----+ + // / +----+--\ + // barSize | | gripSize + // \ +----+--/ + // ----+----+ + // bottomMargin + public FilmStripView(GalleryActivity activity, MediaSet mediaSet, + int topMargin, int midMargin, int bottomMargin, int contentSize, + int thumbSize, int barSize, int gripSize, int gripWidth) { + mTopMargin = topMargin; + mMidMargin = midMargin; + mBottomMargin = bottomMargin; + mContentSize = contentSize; + mBarSize = barSize; + mGripSize = gripSize; + + mStripDrawer = new StripDrawer((Context) activity); + mAlbumView = new AlbumView(activity, thumbSize, thumbSize, thumbSize); + mAlbumView.setOverscrollEffect(SlotView.OVERSCROLL_SYSTEM); + mAlbumView.setSelectionDrawer(mStripDrawer); + mAlbumView.setListener(this); + mAlbumView.setUserInteractionListener(this); + mAlbumDataAdapter = new AlbumDataAdapter(activity, mediaSet); + addComponent(mAlbumView); + mScrollBarView = new ScrollBarView(activity.getAndroidContext(), + mGripSize, gripWidth); + mScrollBarView.setListener(this); + addComponent(mScrollBarView); + + mAlbumView.setModel(mAlbumDataAdapter); + mBackgroundTexture = new NinePatchTexture(activity.getAndroidContext(), + R.drawable.navstrip_translucent); + mFilmStripVisible = true; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public void setUserInteractionListener(UserInteractionListener listener) { + mUIListener = listener; + } + + private void setFilmStripVisible(boolean visible) { + if (mFilmStripVisible == visible) return; + mFilmStripVisible = visible; + if (!visible) { + mFilmStripAnimation = new AlphaAnimation(1, 0); + mFilmStripAnimation.setDuration(HIDE_ANIMATION_DURATION); + mFilmStripAnimation.start(); + } else { + mFilmStripAnimation = null; + } + invalidate(); + } + + public void show() { + setFilmStripVisible(true); + } + + public void hide() { + setFilmStripVisible(false); + } + + @Override + protected void onVisibilityChanged(int visibility) { + super.onVisibilityChanged(visibility); + if (visibility == GLView.VISIBLE) { + onUserInteraction(); + } + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + int height = mTopMargin + mContentSize + mMidMargin + mBarSize + mBottomMargin; + MeasureHelper.getInstance(this) + .setPreferredContentSize(MeasureSpec.getSize(widthSpec), height) + .measure(widthSpec, heightSpec); + } + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + if (!changed) return; + mAlbumView.layout(0, mTopMargin, right - left, mTopMargin + mContentSize); + int barStart = mTopMargin + mContentSize + mMidMargin; + mScrollBarView.layout(0, barStart, right - left, barStart + mBarSize); + int width = right - left; + int height = bottom - top; + } + + @Override + protected boolean onTouch(MotionEvent event) { + // consume all touch events on the "gray area", so they don't go to + // the photo view below. (otherwise you can scroll the picture through + // it). + return true; + } + + @Override + protected boolean dispatchTouchEvent(MotionEvent event) { + if (!mFilmStripVisible && mFilmStripAnimation == null) { + return false; + } + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + onUserInteractionBegin(); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + onUserInteractionEnd(); + break; + } + + return super.dispatchTouchEvent(event); + } + + @Override + protected void render(GLCanvas canvas) { + CanvasAnimation anim = mFilmStripAnimation; + if (anim == null && !mFilmStripVisible) return; + + boolean needRestore = false; + if (anim != null) { + needRestore = true; + canvas.save(anim.getCanvasSaveFlags()); + long now = canvas.currentAnimationTimeMillis(); + boolean more = anim.calculate(now); + anim.apply(canvas); + if (more) { + invalidate(); + } else { + mFilmStripAnimation = null; + } + } + + mBackgroundTexture.draw(canvas, 0, 0, getWidth(), getHeight()); + super.render(canvas); + + if (needRestore) { + canvas.restore(); + } + } + + // Called by AlbumView + public void onSingleTapUp(int slotIndex) { + mAlbumView.setFocusIndex(slotIndex); + mListener.onSlotSelected(slotIndex); + } + + // Called by AlbumView + public void onLongTap(int slotIndex) { + onSingleTapUp(slotIndex); + } + + // Called by AlbumView + public void onUserInteractionBegin() { + mUIListener.onUserInteractionBegin(); + } + + // Called by AlbumView + public void onUserInteractionEnd() { + mUIListener.onUserInteractionEnd(); + } + + // Called by AlbumView + public void onUserInteraction() { + mUIListener.onUserInteraction(); + } + + // Called by AlbumView + public void onScrollPositionChanged(int position, int total) { + mScrollBarView.setContentPosition(position, total); + } + + // Called by ScrollBarView + public void onScrollBarPositionChanged(int position) { + mAlbumView.setScrollPosition(position); + } + + public void setFocusIndex(int slotIndex) { + mAlbumView.setFocusIndex(slotIndex); + mAlbumView.makeSlotVisible(slotIndex); + } + + public void setStartIndex(int slotIndex) { + mAlbumView.setStartIndex(slotIndex); + } + + public void pause() { + mAlbumView.pause(); + mAlbumDataAdapter.pause(); + } + + public void resume() { + mAlbumView.resume(); + mAlbumDataAdapter.resume(); + } +} diff --git a/src/com/android/gallery3d/ui/GLCanvas.java b/src/com/android/gallery3d/ui/GLCanvas.java new file mode 100644 index 000000000..88c02f3b5 --- /dev/null +++ b/src/com/android/gallery3d/ui/GLCanvas.java @@ -0,0 +1,138 @@ +/* + * 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.ui; + +import android.graphics.RectF; + +import javax.microedition.khronos.opengles.GL11; + +// +// GLCanvas gives a convenient interface to draw using OpenGL. +// +// When a rectangle is specified in this interface, it means the region +// [x, x+width) * [y, y+height) +// +public interface GLCanvas { + // Tells GLCanvas the size of the underlying GL surface. This should be + // called before first drawing and when the size of GL surface is changed. + // This is called by GLRoot and should not be called by the clients + // who only want to draw on the GLCanvas. Both width and height must be + // nonnegative. + public void setSize(int width, int height); + + // Clear the drawing buffers. This should only be used by GLRoot. + public void clearBuffer(); + + // This is the time value used to calculate the animation in the current + // frame. The "set" function should only called by GLRoot, and the + // "time" parameter must be nonnegative. + public void setCurrentAnimationTimeMillis(long time); + public long currentAnimationTimeMillis(); + + public void setBlendEnabled(boolean enabled); + + // Sets and gets the current alpha, alpha must be in [0, 1]. + public void setAlpha(float alpha); + public float getAlpha(); + + // (current alpha) = (current alpha) * alpha + public void multiplyAlpha(float alpha); + + // Change the current transform matrix. + public void translate(float x, float y, float z); + public void scale(float sx, float sy, float sz); + public void rotate(float angle, float x, float y, float z); + public void multiplyMatrix(float[] mMatrix, int offset); + + // Modifies the current clip with the specified rectangle. + // (current clip) = (current clip) intersect (specified rectangle). + // Returns true if the result clip is non-empty. + public boolean clipRect(int left, int top, int right, int bottom); + + // Pushes the configuration state (matrix, alpha, and clip) onto + // a private stack. + public int save(); + + // Same as save(), but only save those specified in saveFlags. + public int save(int saveFlags); + + public static final int SAVE_FLAG_ALL = 0xFFFFFFFF; + public static final int SAVE_FLAG_CLIP = 0x01; + public static final int SAVE_FLAG_ALPHA = 0x02; + public static final int SAVE_FLAG_MATRIX = 0x04; + + // Pops from the top of the stack as current configuration state (matrix, + // alpha, and clip). This call balances a previous call to save(), and is + // used to remove all modifications to the configuration state since the + // last save call. + public void restore(); + + // Draws a line using the specified paint from (x1, y1) to (x2, y2). + // (Both end points are included). + public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint); + + // Draws a rectangle using the specified paint from (x1, y1) to (x2, y2). + // (Both end points are included). + public void drawRect(float x1, float y1, float x2, float y2, GLPaint paint); + + // Fills the specified rectangle with the specified color. + public void fillRect(float x, float y, float width, float height, int color); + + // Draws a texture to the specified rectangle. + public void drawTexture( + BasicTexture texture, int x, int y, int width, int height); + public void drawMesh(BasicTexture tex, int x, int y, int xyBuffer, + int uvBuffer, int indexBuffer, int indexCount); + + // Draws a texture to the specified rectangle. The "alpha" parameter + // overrides the current drawing alpha value. + public void drawTexture(BasicTexture texture, + int x, int y, int width, int height, float alpha); + + // Draws a the source rectangle part of the texture to the target rectangle. + public void drawTexture(BasicTexture texture, RectF source, RectF target); + + // Draw two textures to the specified rectangle. The actual texture used is + // from * (1 - ratio) + to * ratio + // The two textures must have the same size. + public void drawMixed(BasicTexture from, BasicTexture to, + float ratio, int x, int y, int w, int h); + + public void drawMixed(BasicTexture from, int toColor, + float ratio, int x, int y, int w, int h); + + // Return a texture copied from the specified rectangle. + public BasicTexture copyTexture(int x, int y, int width, int height); + + // Gets the underlying GL instance. This is used only when direct access to + // GL is needed. + public GL11 getGLInstance(); + + // Unloads the specified texture from the canvas. The resource allocated + // to draw the texture will be released. The specified texture will return + // to the unloaded state. This function should be called only from + // BasicTexture or its descendant + public boolean unloadTexture(BasicTexture texture); + + // Delete the specified buffer object, similar to unloadTexture. + public void deleteBuffer(int bufferId); + + // Delete the textures and buffers in GL side. This function should only be + // called in the GL thread. + public void deleteRecycledResources(); + +} diff --git a/src/com/android/gallery3d/ui/GLCanvasImpl.java b/src/com/android/gallery3d/ui/GLCanvasImpl.java new file mode 100644 index 000000000..387743f5d --- /dev/null +++ b/src/com/android/gallery3d/ui/GLCanvasImpl.java @@ -0,0 +1,913 @@ +/* + * 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.ui; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.IntArray; + +import android.graphics.Rect; +import android.graphics.RectF; +import android.opengl.GLU; +import android.opengl.Matrix; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.Stack; +import javax.microedition.khronos.opengles.GL10; +import javax.microedition.khronos.opengles.GL11; +import javax.microedition.khronos.opengles.GL11Ext; + +public class GLCanvasImpl implements GLCanvas { + @SuppressWarnings("unused") + private static final String TAG = "GLCanvasImp"; + + private static final float OPAQUE_ALPHA = 0.95f; + + private static final int OFFSET_FILL_RECT = 0; + private static final int OFFSET_DRAW_LINE = 4; + private static final int OFFSET_DRAW_RECT = 6; + private static final float[] BOX_COORDINATES = { + 0, 0, 1, 0, 0, 1, 1, 1, // used for filling a rectangle + 0, 0, 1, 1, // used for drawing a line + 0, 0, 0, 1, 1, 1, 1, 0}; // used for drawing the outline of a rectangle + + private final GL11 mGL; + + private final float mMatrixValues[] = new float[16]; + private final float mTextureMatrixValues[] = new float[16]; + + // mapPoints needs 10 input and output numbers. + private final float mMapPointsBuffer[] = new float[10]; + + private final float mTextureColor[] = new float[4]; + + private int mBoxCoords; + + private final GLState mGLState; + + private long mAnimationTime; + + private float mAlpha; + private final Rect mClipRect = new Rect(); + private final Stack<ConfigState> mRestoreStack = + new Stack<ConfigState>(); + private ConfigState mRecycledRestoreAction; + + private final RectF mDrawTextureSourceRect = new RectF(); + private final RectF mDrawTextureTargetRect = new RectF(); + private final float[] mTempMatrix = new float[32]; + private final IntArray mUnboundTextures = new IntArray(); + private final IntArray mDeleteBuffers = new IntArray(); + private int mHeight; + private boolean mBlendEnabled = true; + + // Drawing statistics + int mCountDrawLine; + int mCountFillRect; + int mCountDrawMesh; + int mCountTextureRect; + int mCountTextureOES; + + GLCanvasImpl(GL11 gl) { + mGL = gl; + mGLState = new GLState(gl); + initialize(); + } + + public void setSize(int width, int height) { + Utils.assertTrue(width >= 0 && height >= 0); + mHeight = height; + + GL11 gl = mGL; + gl.glViewport(0, 0, width, height); + gl.glMatrixMode(GL11.GL_PROJECTION); + gl.glLoadIdentity(); + GLU.gluOrtho2D(gl, 0, width, 0, height); + + gl.glMatrixMode(GL11.GL_MODELVIEW); + gl.glLoadIdentity(); + float matrix[] = mMatrixValues; + + Matrix.setIdentityM(matrix, 0); + Matrix.translateM(matrix, 0, 0, mHeight, 0); + Matrix.scaleM(matrix, 0, 1, -1, 1); + + mClipRect.set(0, 0, width, height); + gl.glScissor(0, 0, width, height); + } + + public long currentAnimationTimeMillis() { + return mAnimationTime; + } + + public void setAlpha(float alpha) { + Utils.assertTrue(alpha >= 0 && alpha <= 1); + mAlpha = alpha; + } + + public void multiplyAlpha(float alpha) { + Utils.assertTrue(alpha >= 0 && alpha <= 1); + mAlpha *= alpha; + } + + public float getAlpha() { + return mAlpha; + } + + private static ByteBuffer allocateDirectNativeOrderBuffer(int size) { + return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()); + } + + private void initialize() { + GL11 gl = mGL; + + // First create an nio buffer, then create a VBO from it. + int size = BOX_COORDINATES.length * Float.SIZE / Byte.SIZE; + FloatBuffer xyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer(); + xyBuffer.put(BOX_COORDINATES, 0, BOX_COORDINATES.length).position(0); + + int[] name = new int[1]; + gl.glGenBuffers(1, name, 0); + mBoxCoords = name[0]; + + gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords); + gl.glBufferData(GL11.GL_ARRAY_BUFFER, + xyBuffer.capacity() * (Float.SIZE / Byte.SIZE), + xyBuffer, GL11.GL_STATIC_DRAW); + + gl.glVertexPointer(2, GL11.GL_FLOAT, 0, 0); + gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); + + // Enable the texture coordinate array for Texture 1 + gl.glClientActiveTexture(GL11.GL_TEXTURE1); + gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); + gl.glClientActiveTexture(GL11.GL_TEXTURE0); + gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); + + // mMatrixValues will be initialized in setSize() + mAlpha = 1.0f; + } + + public void drawRect(float x, float y, float width, float height, GLPaint paint) { + GL11 gl = mGL; + + mGLState.setColorMode(paint.getColor(), mAlpha); + mGLState.setLineWidth(paint.getLineWidth()); + mGLState.setLineSmooth(paint.getAntiAlias()); + + saveTransform(); + translate(x, y, 0); + scale(width, height, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_LINE_LOOP, OFFSET_DRAW_RECT, 4); + + restoreTransform(); + mCountDrawLine++; + } + + public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) { + GL11 gl = mGL; + + mGLState.setColorMode(paint.getColor(), mAlpha); + mGLState.setLineWidth(paint.getLineWidth()); + mGLState.setLineSmooth(paint.getAntiAlias()); + + saveTransform(); + translate(x1, y1, 0); + scale(x2 - x1, y2 - y1, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_LINE_STRIP, OFFSET_DRAW_LINE, 2); + + restoreTransform(); + mCountDrawLine++; + } + + public void fillRect(float x, float y, float width, float height, int color) { + mGLState.setColorMode(color, mAlpha); + GL11 gl = mGL; + + saveTransform(); + translate(x, y, 0); + scale(width, height, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4); + + restoreTransform(); + mCountFillRect++; + } + + public void translate(float x, float y, float z) { + Matrix.translateM(mMatrixValues, 0, x, y, z); + } + + public void scale(float sx, float sy, float sz) { + Matrix.scaleM(mMatrixValues, 0, sx, sy, sz); + } + + public void rotate(float angle, float x, float y, float z) { + float[] temp = mTempMatrix; + Matrix.setRotateM(temp, 0, angle, x, y, z); + Matrix.multiplyMM(temp, 16, mMatrixValues, 0, temp, 0); + System.arraycopy(temp, 16, mMatrixValues, 0, 16); + } + + public void multiplyMatrix(float matrix[], int offset) { + float[] temp = mTempMatrix; + Matrix.multiplyMM(temp, 0, mMatrixValues , 0, matrix, 0); + System.arraycopy(temp, 0, mMatrixValues, 0, 16); + } + + private void textureRect(float x, float y, float width, float height) { + GL11 gl = mGL; + + saveTransform(); + translate(x, y, 0); + scale(width, height, 1); + + gl.glLoadMatrixf(mMatrixValues, 0); + gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4); + + restoreTransform(); + mCountTextureRect++; + } + + public void drawMesh(BasicTexture tex, int x, int y, int xyBuffer, + int uvBuffer, int indexBuffer, int indexCount) { + float alpha = mAlpha; + if (!bindTexture(tex)) return; + + mGLState.setBlendEnabled(mBlendEnabled + && (!tex.isOpaque() || alpha < OPAQUE_ALPHA)); + mGLState.setTextureAlpha(alpha); + + // Reset the texture matrix. We will set our own texture coordinates + // below. + setTextureCoords(0, 0, 1, 1); + + saveTransform(); + translate(x, y, 0); + + mGL.glLoadMatrixf(mMatrixValues, 0); + + mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, xyBuffer); + mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0); + + mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, uvBuffer); + mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); + + mGL.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, indexBuffer); + mGL.glDrawElements(GL11.GL_TRIANGLE_STRIP, + indexCount, GL11.GL_UNSIGNED_BYTE, 0); + + mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords); + mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0); + mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0); + + restoreTransform(); + mCountDrawMesh++; + } + + private float[] mapPoints(float matrix[], int x1, int y1, int x2, int y2) { + float[] point = mMapPointsBuffer; + int srcOffset = 6; + point[srcOffset] = x1; + point[srcOffset + 1] = y1; + point[srcOffset + 2] = 0; + point[srcOffset + 3] = 1; + + int resultOffset = 0; + Matrix.multiplyMV(point, resultOffset, matrix, 0, point, srcOffset); + point[resultOffset] /= point[resultOffset + 3]; + point[resultOffset + 1] /= point[resultOffset + 3]; + + // map the second point + point[srcOffset] = x2; + point[srcOffset + 1] = y2; + resultOffset = 2; + Matrix.multiplyMV(point, resultOffset, matrix, 0, point, srcOffset); + point[resultOffset] /= point[resultOffset + 3]; + point[resultOffset + 1] /= point[resultOffset + 3]; + + return point; + } + + public boolean clipRect(int left, int top, int right, int bottom) { + float point[] = mapPoints(mMatrixValues, left, top, right, bottom); + + // mMatrix could be a rotation matrix. In this case, we need to find + // the boundaries after rotation. (only handle 90 * n degrees) + if (point[0] > point[2]) { + left = (int) point[2]; + right = (int) point[0]; + } else { + left = (int) point[0]; + right = (int) point[2]; + } + if (point[1] > point[3]) { + top = (int) point[3]; + bottom = (int) point[1]; + } else { + top = (int) point[1]; + bottom = (int) point[3]; + } + Rect clip = mClipRect; + + boolean intersect = clip.intersect(left, top, right, bottom); + if (!intersect) clip.set(0, 0, 0, 0); + mGL.glScissor(clip.left, clip.top, clip.width(), clip.height()); + return intersect; + } + + private void drawBoundTexture( + BasicTexture texture, int x, int y, int width, int height) { + // Test whether it has been rotated or flipped, if so, glDrawTexiOES + // won't work + if (isMatrixRotatedOrFlipped(mMatrixValues)) { + setTextureCoords(0, 0, + (float) texture.getWidth() / texture.getTextureWidth(), + (float) texture.getHeight() / texture.getTextureHeight()); + textureRect(x, y, width, height); + } else { + // draw the rect from bottom-left to top-right + float points[] = mapPoints( + mMatrixValues, x, y + height, x + width, y); + x = Math.round(points[0]); + y = Math.round(points[1]); + width = Math.round(points[2]) - x; + height = Math.round(points[3]) - y; + if (width > 0 && height > 0) { + ((GL11Ext) mGL).glDrawTexiOES(x, y, 0, width, height); + mCountTextureOES++; + } + } + } + + public void drawTexture( + BasicTexture texture, int x, int y, int width, int height) { + drawTexture(texture, x, y, width, height, mAlpha); + } + + public void setBlendEnabled(boolean enabled) { + mBlendEnabled = enabled; + } + + public void drawTexture(BasicTexture texture, + int x, int y, int width, int height, float alpha) { + if (width <= 0 || height <= 0) return; + + mGLState.setBlendEnabled(mBlendEnabled + && (!texture.isOpaque() || alpha < OPAQUE_ALPHA)); + if (!bindTexture(texture)) return; + mGLState.setTextureAlpha(alpha); + drawBoundTexture(texture, x, y, width, height); + } + + public void drawTexture(BasicTexture texture, RectF source, RectF target) { + if (target.width() <= 0 || target.height() <= 0) return; + + // Copy the input to avoid changing it. + mDrawTextureSourceRect.set(source); + mDrawTextureTargetRect.set(target); + source = mDrawTextureSourceRect; + target = mDrawTextureTargetRect; + + mGLState.setBlendEnabled(mBlendEnabled + && (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA)); + if (!bindTexture(texture)) return; + convertCoordinate(source, target, texture); + setTextureCoords(source); + mGLState.setTextureAlpha(mAlpha); + textureRect(target.left, target.top, target.width(), target.height()); + } + + // This function changes the source coordinate to the texture coordinates. + // It also clips the source and target coordinates if it is beyond the + // bound of the texture. + private void convertCoordinate(RectF source, RectF target, + BasicTexture texture) { + + int width = texture.getWidth(); + int height = texture.getHeight(); + int texWidth = texture.getTextureWidth(); + int texHeight = texture.getTextureHeight(); + // Convert to texture coordinates + source.left /= texWidth; + source.right /= texWidth; + source.top /= texHeight; + source.bottom /= texHeight; + + // Clip if the rendering range is beyond the bound of the texture. + float xBound = (float) width / texWidth; + if (source.right > xBound) { + target.right = target.left + target.width() * + (xBound - source.left) / source.width(); + source.right = xBound; + } + float yBound = (float) height / texHeight; + if (source.bottom > yBound) { + target.bottom = target.top + target.height() * + (yBound - source.top) / source.height(); + source.bottom = yBound; + } + } + + public void drawMixed(BasicTexture from, + int toColor, float ratio, int x, int y, int w, int h) { + drawMixed(from, toColor, ratio, x, y, w, h, mAlpha); + } + + public void drawMixed(BasicTexture from, BasicTexture to, + float ratio, int x, int y, int w, int h) { + drawMixed(from, to, ratio, x, y, w, h, mAlpha); + } + + private boolean bindTexture(BasicTexture texture) { + if (!texture.onBind(this)) return false; + mGLState.setTexture2DEnabled(true); + mGL.glBindTexture(GL11.GL_TEXTURE_2D, texture.getId()); + return true; + } + + private void setTextureColor(float r, float g, float b, float alpha) { + float[] color = mTextureColor; + color[0] = r; + color[1] = g; + color[2] = b; + color[3] = alpha; + } + + private void drawMixed(BasicTexture from, int toColor, + float ratio, int x, int y, int width, int height, float alpha) { + + if (ratio <= 0) { + drawTexture(from, x, y, width, height, alpha); + return; + } else if (ratio >= 1) { + fillRect(x, y, width, height, toColor); + return; + } + + mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque() + || !Utils.isOpaque(toColor) || alpha < OPAQUE_ALPHA)); + + final GL11 gl = mGL; + if (!bindTexture(from)) return; + + // + // The formula we want: + // alpha * ((1 - ratio) * from + ratio * to) + // The formula that GL supports is in the form of: + // combo * (modulate * from) + (1 - combo) * to + // + // So, we have combo = 1 - alpha * ratio + // and modulate = alpha * (1f - ratio) / combo + // + float comboRatio = 1 - alpha * ratio; + + // handle the case that (1 - comboRatio) == 0 + if (alpha < OPAQUE_ALPHA) { + mGLState.setTextureAlpha(alpha * (1f - ratio) / comboRatio); + } else { + mGLState.setTextureAlpha(1f); + } + + // Interpolate the RGB and alpha values between both textures. + mGLState.setTexEnvMode(GL11.GL_COMBINE); + // Specify the interpolation factor via the alpha component of + // GL_TEXTURE_ENV_COLORs. + // RGB component are get from toColor and will used as SRC1 + float colorAlpha = (float) (toColor >>> 24) / (0xff * 0xff); + setTextureColor(((toColor >>> 16) & 0xff) * colorAlpha, + ((toColor >>> 8) & 0xff) * colorAlpha, + (toColor & 0xff) * colorAlpha, comboRatio); + gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, mTextureColor, 0); + + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_RGB, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_RGB, GL11.GL_SRC_COLOR); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_ALPHA, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_ALPHA, GL11.GL_SRC_ALPHA); + + // Wire up the interpolation factor for RGB. + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA); + + // Wire up the interpolation factor for alpha. + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA); + + drawBoundTexture(from, x, y, width, height); + mGLState.setTexEnvMode(GL11.GL_REPLACE); + } + + private void drawMixed(BasicTexture from, BasicTexture to, + float ratio, int x, int y, int width, int height, float alpha) { + + if (ratio <= 0) { + drawTexture(from, x, y, width, height, alpha); + return; + } else if (ratio >= 1) { + drawTexture(to, x, y, width, height, alpha); + return; + } + + // In the current implementation the two textures must have the + // same size. + Utils.assertTrue(from.getWidth() == to.getWidth() + && from.getHeight() == to.getHeight()); + + mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque() + || !to.isOpaque() || alpha < OPAQUE_ALPHA)); + + final GL11 gl = mGL; + if (!bindTexture(from)) return; + + // + // The formula we want: + // alpha * ((1 - ratio) * from + ratio * to) + // The formula that GL supports is in the form of: + // combo * (modulate * from) + (1 - combo) * to + // + // So, we have combo = 1 - alpha * ratio + // and modulate = alpha * (1f - ratio) / combo + // + float comboRatio = 1 - alpha * ratio; + + // handle the case that (1 - comboRatio) == 0 + if (alpha < OPAQUE_ALPHA) { + mGLState.setTextureAlpha(alpha * (1f - ratio) / comboRatio); + } else { + mGLState.setTextureAlpha(1f); + } + + gl.glActiveTexture(GL11.GL_TEXTURE1); + if (!bindTexture(to)) { + // Disable TEXTURE1. + gl.glDisable(GL11.GL_TEXTURE_2D); + // Switch back to the default texture unit. + gl.glActiveTexture(GL11.GL_TEXTURE0); + return; + } + gl.glEnable(GL11.GL_TEXTURE_2D); + + // Interpolate the RGB and alpha values between both textures. + mGLState.setTexEnvMode(GL11.GL_COMBINE); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE); + + // Specify the interpolation factor via the alpha component of + // GL_TEXTURE_ENV_COLORs. + // We don't use the RGB color, so just give them 0s. + setTextureColor(0, 0, 0, comboRatio); + gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, mTextureColor, 0); + + // Wire up the interpolation factor for RGB. + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA); + + // Wire up the interpolation factor for alpha. + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT); + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA); + + // Draw the combined texture. + drawBoundTexture(to, x, y, width, height); + + // Disable TEXTURE1. + gl.glDisable(GL11.GL_TEXTURE_2D); + // Switch back to the default texture unit. + gl.glActiveTexture(GL11.GL_TEXTURE0); + } + + // TODO: the code only work for 2D should get fixed for 3D or removed + private static final int MSKEW_X = 4; + private static final int MSKEW_Y = 1; + private static final int MSCALE_X = 0; + private static final int MSCALE_Y = 5; + + private static boolean isMatrixRotatedOrFlipped(float matrix[]) { + final float eps = 1e-5f; + return Math.abs(matrix[MSKEW_X]) > eps + || Math.abs(matrix[MSKEW_Y]) > eps + || matrix[MSCALE_X] < -eps + || matrix[MSCALE_Y] > eps; + } + + public BasicTexture copyTexture(int x, int y, int width, int height) { + + if (isMatrixRotatedOrFlipped(mMatrixValues)) { + throw new IllegalArgumentException("cannot support rotated matrix"); + } + float points[] = mapPoints(mMatrixValues, x, y + height, x + width, y); + x = (int) points[0]; + y = (int) points[1]; + width = (int) points[2] - x; + height = (int) points[3] - y; + + GL11 gl = mGL; + + RawTexture texture = RawTexture.newInstance(this); + gl.glBindTexture(GL11.GL_TEXTURE_2D, texture.getId()); + texture.setSize(width, height); + + int[] cropRect = {0, 0, width, height}; + gl.glTexParameteriv(GL11.GL_TEXTURE_2D, + GL11Ext.GL_TEXTURE_CROP_RECT_OES, cropRect, 0); + gl.glTexParameteri(GL11.GL_TEXTURE_2D, + GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE); + gl.glTexParameteri(GL11.GL_TEXTURE_2D, + GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE); + gl.glTexParameterf(GL11.GL_TEXTURE_2D, + GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + gl.glTexParameterf(GL11.GL_TEXTURE_2D, + GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + gl.glCopyTexImage2D(GL11.GL_TEXTURE_2D, 0, + GL11.GL_RGB, x, y, texture.getTextureWidth(), + texture.getTextureHeight(), 0); + + return texture; + } + + private static class GLState { + + private final GL11 mGL; + + private int mTexEnvMode = GL11.GL_REPLACE; + private float mTextureAlpha = 1.0f; + private boolean mTexture2DEnabled = true; + private boolean mBlendEnabled = true; + private float mLineWidth = 1.0f; + private boolean mLineSmooth = false; + + public GLState(GL11 gl) { + mGL = gl; + + // Disable unused state + gl.glDisable(GL11.GL_LIGHTING); + + // Enable used features + gl.glEnable(GL11.GL_DITHER); + gl.glEnable(GL11.GL_SCISSOR_TEST); + + gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); + gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); + gl.glEnable(GL11.GL_TEXTURE_2D); + + gl.glTexEnvf(GL11.GL_TEXTURE_ENV, + GL11.GL_TEXTURE_ENV_MODE, GL11.GL_REPLACE); + + // Set the background color + gl.glClearColor(0f, 0f, 0f, 0f); + gl.glClearStencil(0); + + gl.glEnable(GL11.GL_BLEND); + gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA); + + // We use 565 or 8888 format, so set the alignment to 2 bytes/pixel. + gl.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 2); + } + + public void setTexEnvMode(int mode) { + if (mTexEnvMode == mode) return; + mTexEnvMode = mode; + mGL.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, mode); + } + + public void setLineWidth(float width) { + if (mLineWidth == width) return; + mLineWidth = width; + mGL.glLineWidth(width); + } + + public void setLineSmooth(boolean enabled) { + if (mLineSmooth == enabled) return; + mLineSmooth = enabled; + if (enabled) { + mGL.glEnable(GL11.GL_LINE_SMOOTH); + } else { + mGL.glDisable(GL11.GL_LINE_SMOOTH); + } + } + + public void setTextureAlpha(float alpha) { + if (mTextureAlpha == alpha) return; + mTextureAlpha = alpha; + if (alpha >= OPAQUE_ALPHA) { + // The alpha is need for those texture without alpha channel + mGL.glColor4f(1, 1, 1, 1); + setTexEnvMode(GL11.GL_REPLACE); + } else { + mGL.glColor4f(alpha, alpha, alpha, alpha); + setTexEnvMode(GL11.GL_MODULATE); + } + } + + public void setColorMode(int color, float alpha) { + setBlendEnabled(!Utils.isOpaque(color) || alpha < OPAQUE_ALPHA); + + // Set mTextureAlpha to an invalid value, so that it will reset + // again in setTextureAlpha(float) later. + mTextureAlpha = -1.0f; + + setTexture2DEnabled(false); + + float prealpha = (color >>> 24) * alpha * 65535f / 255f / 255f; + mGL.glColor4x( + Math.round(((color >> 16) & 0xFF) * prealpha), + Math.round(((color >> 8) & 0xFF) * prealpha), + Math.round((color & 0xFF) * prealpha), + Math.round(255 * prealpha)); + } + + public void setTexture2DEnabled(boolean enabled) { + if (mTexture2DEnabled == enabled) return; + mTexture2DEnabled = enabled; + if (enabled) { + mGL.glEnable(GL11.GL_TEXTURE_2D); + } else { + mGL.glDisable(GL11.GL_TEXTURE_2D); + } + } + + public void setBlendEnabled(boolean enabled) { + if (mBlendEnabled == enabled) return; + mBlendEnabled = enabled; + if (enabled) { + mGL.glEnable(GL11.GL_BLEND); + } else { + mGL.glDisable(GL11.GL_BLEND); + } + } + } + + public GL11 getGLInstance() { + return mGL; + } + + public void setCurrentAnimationTimeMillis(long time) { + Utils.assertTrue(time >= 0); + mAnimationTime = time; + } + + public void clearBuffer() { + mGL.glClear(GL10.GL_COLOR_BUFFER_BIT); + } + + private void setTextureCoords(RectF source) { + setTextureCoords(source.left, source.top, source.right, source.bottom); + } + + private void setTextureCoords(float left, float top, + float right, float bottom) { + mGL.glMatrixMode(GL11.GL_TEXTURE); + mTextureMatrixValues[0] = right - left; + mTextureMatrixValues[5] = bottom - top; + mTextureMatrixValues[10] = 1; + mTextureMatrixValues[12] = left; + mTextureMatrixValues[13] = top; + mTextureMatrixValues[15] = 1; + mGL.glLoadMatrixf(mTextureMatrixValues, 0); + mGL.glMatrixMode(GL11.GL_MODELVIEW); + } + + // unloadTexture and deleteBuffer can be called from the finalizer thread, + // so we synchronized on the mUnboundTextures object. + public boolean unloadTexture(BasicTexture t) { + synchronized (mUnboundTextures) { + if (!t.isLoaded(this)) return false; + mUnboundTextures.add(t.mId); + return true; + } + } + + public void deleteBuffer(int bufferId) { + synchronized (mUnboundTextures) { + mDeleteBuffers.add(bufferId); + } + } + + public void deleteRecycledResources() { + synchronized (mUnboundTextures) { + IntArray ids = mUnboundTextures; + if (ids.size() > 0) { + mGL.glDeleteTextures(ids.size(), ids.getInternalArray(), 0); + ids.clear(); + } + + ids = mDeleteBuffers; + if (ids.size() > 0) { + mGL.glDeleteBuffers(ids.size(), ids.getInternalArray(), 0); + ids.clear(); + } + } + } + + public int save() { + return save(SAVE_FLAG_ALL); + } + + public int save(int saveFlags) { + ConfigState config = obtainRestoreConfig(); + + if ((saveFlags & SAVE_FLAG_ALPHA) != 0) { + config.mAlpha = mAlpha; + } else { + config.mAlpha = -1; + } + + if ((saveFlags & SAVE_FLAG_CLIP) != 0) { + config.mRect.set(mClipRect); + } else { + config.mRect.left = Integer.MAX_VALUE; + } + + if ((saveFlags & SAVE_FLAG_MATRIX) != 0) { + System.arraycopy(mMatrixValues, 0, config.mMatrix, 0, 16); + } else { + config.mMatrix[0] = Float.NEGATIVE_INFINITY; + } + + mRestoreStack.push(config); + return mRestoreStack.size() - 1; + } + + public void restore() { + if (mRestoreStack.isEmpty()) throw new IllegalStateException(); + ConfigState config = mRestoreStack.pop(); + config.restore(this); + freeRestoreConfig(config); + } + + private void freeRestoreConfig(ConfigState action) { + action.mNextFree = mRecycledRestoreAction; + mRecycledRestoreAction = action; + } + + private ConfigState obtainRestoreConfig() { + if (mRecycledRestoreAction != null) { + ConfigState result = mRecycledRestoreAction; + mRecycledRestoreAction = result.mNextFree; + return result; + } + return new ConfigState(); + } + + private static class ConfigState { + float mAlpha; + Rect mRect = new Rect(); + float mMatrix[] = new float[16]; + ConfigState mNextFree; + + public void restore(GLCanvasImpl canvas) { + if (mAlpha >= 0) canvas.setAlpha(mAlpha); + if (mRect.left != Integer.MAX_VALUE) { + Rect rect = mRect; + canvas.mClipRect.set(rect); + canvas.mGL.glScissor( + rect.left, rect.top, rect.width(), rect.height()); + } + if (mMatrix[0] != Float.NEGATIVE_INFINITY) { + System.arraycopy(mMatrix, 0, canvas.mMatrixValues, 0, 16); + } + } + } + + public void dumpStatisticsAndClear() { + String line = String.format( + "MESH:%d, TEX_OES:%d, TEX_RECT:%d, FILL_RECT:%d, LINE:%d", + mCountDrawMesh, mCountTextureRect, mCountTextureOES, + mCountFillRect, mCountDrawLine); + mCountDrawMesh = 0; + mCountTextureRect = 0; + mCountTextureOES = 0; + mCountFillRect = 0; + mCountDrawLine = 0; + Log.d(TAG, line); + } + + private void saveTransform() { + System.arraycopy(mMatrixValues, 0, mTempMatrix, 0, 16); + } + + private void restoreTransform() { + System.arraycopy(mTempMatrix, 0, mMatrixValues, 0, 16); + } +} diff --git a/src/com/android/gallery3d/ui/GLPaint.java b/src/com/android/gallery3d/ui/GLPaint.java new file mode 100644 index 000000000..9f7b6f1f3 --- /dev/null +++ b/src/com/android/gallery3d/ui/GLPaint.java @@ -0,0 +1,65 @@ +/* + * 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.ui; + +import com.android.gallery3d.common.Utils; + + +public class GLPaint { + public static final int FLAG_ANTI_ALIAS = 0x01; + + private int mFlags = 0; + private float mLineWidth = 1f; + private int mColor = 0; + + public int getFlags() { + return mFlags; + } + + public void setFlags(int flags) { + mFlags = flags; + } + + public void setColor(int color) { + mColor = color; + } + + public int getColor() { + return mColor; + } + + public void setLineWidth(float width) { + Utils.assertTrue(width >= 0); + mLineWidth = width; + } + + public float getLineWidth() { + return mLineWidth; + } + + public void setAntiAlias(boolean enabled) { + if (enabled) { + mFlags |= FLAG_ANTI_ALIAS; + } else { + mFlags &= ~FLAG_ANTI_ALIAS; + } + } + + public boolean getAntiAlias(){ + return (mFlags & FLAG_ANTI_ALIAS) != 0; + } +} diff --git a/src/com/android/gallery3d/ui/GLRoot.java b/src/com/android/gallery3d/ui/GLRoot.java new file mode 100644 index 000000000..24e5794b0 --- /dev/null +++ b/src/com/android/gallery3d/ui/GLRoot.java @@ -0,0 +1,37 @@ +/* + * 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.ui; + +import com.android.gallery3d.anim.CanvasAnimation; + +public interface GLRoot { + + public static interface OnGLIdleListener { + public boolean onGLIdle(GLRoot root, GLCanvas canvas); + } + + public void addOnGLIdleListener(OnGLIdleListener listener); + public void registerLaunchedAnimation(CanvasAnimation animation); + public void requestRender(); + public void requestLayoutContentPane(); + public boolean hasStencil(); + + public void lockRenderThread(); + public void unlockRenderThread(); + + public void setContentPane(GLView content); +} diff --git a/src/com/android/gallery3d/ui/GLRootView.java b/src/com/android/gallery3d/ui/GLRootView.java new file mode 100644 index 000000000..e03adf1c4 --- /dev/null +++ b/src/com/android/gallery3d/ui/GLRootView.java @@ -0,0 +1,414 @@ +/* + * 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.ui; + +import com.android.gallery3d.anim.CanvasAnimation; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.GalleryUtils; + +import android.app.Activity; +import android.content.Context; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.opengl.GLSurfaceView; +import android.os.Process; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.MotionEvent; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.concurrent.locks.ReentrantLock; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; +import javax.microedition.khronos.opengles.GL11; + +// The root component of all <code>GLView</code>s. The rendering is done in GL +// thread while the event handling is done in the main thread. To synchronize +// the two threads, the entry points of this package need to synchronize on the +// <code>GLRootView</code> instance unless it can be proved that the rendering +// thread won't access the same thing as the method. The entry points include: +// (1) The public methods of HeadUpDisplay +// (2) The public methods of CameraHeadUpDisplay +// (3) The overridden methods in GLRootView. +public class GLRootView extends GLSurfaceView + implements GLSurfaceView.Renderer, GLRoot { + private static final String TAG = "GLRootView"; + + private static final boolean DEBUG_FPS = false; + private int mFrameCount = 0; + private long mFrameCountingStart = 0; + + private static final boolean DEBUG_INVALIDATE = false; + private int mInvalidateColor = 0; + + private static final boolean DEBUG_DRAWING_STAT = false; + + private static final int FLAG_INITIALIZED = 1; + private static final int FLAG_NEED_LAYOUT = 2; + + private GL11 mGL; + private GLCanvasImpl mCanvas; + + private GLView mContentView; + private DisplayMetrics mDisplayMetrics; + + private int mFlags = FLAG_NEED_LAYOUT; + private volatile boolean mRenderRequested = false; + + private Rect mClipRect = new Rect(); + private int mClipRetryCount = 0; + + private final GalleryEGLConfigChooser mEglConfigChooser = + new GalleryEGLConfigChooser(); + + private final ArrayList<CanvasAnimation> mAnimations = + new ArrayList<CanvasAnimation>(); + + private final LinkedList<OnGLIdleListener> mIdleListeners = + new LinkedList<OnGLIdleListener>(); + + private final IdleRunner mIdleRunner = new IdleRunner(); + + private final ReentrantLock mRenderLock = new ReentrantLock(); + + private static final int TARGET_FRAME_TIME = 33; + private long mLastDrawFinishTime; + private boolean mInDownState = false; + + public GLRootView(Context context) { + this(context, null); + } + + public GLRootView(Context context, AttributeSet attrs) { + super(context, attrs); + mFlags |= FLAG_INITIALIZED; + setBackgroundDrawable(null); + setEGLConfigChooser(mEglConfigChooser); + setRenderer(this); + getHolder().setFormat(PixelFormat.RGB_565); + + // Uncomment this to enable gl error check. + //setDebugFlags(DEBUG_CHECK_GL_ERROR); + } + + public GalleryEGLConfigChooser getEGLConfigChooser() { + return mEglConfigChooser; + } + + @Override + public boolean hasStencil() { + return getEGLConfigChooser().getStencilBits() > 0; + } + + @Override + public void registerLaunchedAnimation(CanvasAnimation animation) { + // Register the newly launched animation so that we can set the start + // time more precisely. (Usually, it takes much longer for first + // rendering, so we set the animation start time as the time we + // complete rendering) + mAnimations.add(animation); + } + + @Override + public void addOnGLIdleListener(OnGLIdleListener listener) { + synchronized (mIdleListeners) { + mIdleListeners.addLast(listener); + mIdleRunner.enable(); + } + } + + @Override + public void setContentPane(GLView content) { + if (mContentView == content) return; + if (mContentView != null) { + if (mInDownState) { + long now = SystemClock.uptimeMillis(); + MotionEvent cancelEvent = MotionEvent.obtain( + now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); + mContentView.dispatchTouchEvent(cancelEvent); + cancelEvent.recycle(); + mInDownState = false; + } + mContentView.detachFromRoot(); + BasicTexture.yieldAllTextures(); + } + mContentView = content; + if (content != null) { + content.attachToRoot(this); + requestLayoutContentPane(); + } + } + + public GLView getContentPane() { + return mContentView; + } + + @Override + public void requestRender() { + if (DEBUG_INVALIDATE) { + StackTraceElement e = Thread.currentThread().getStackTrace()[4]; + String caller = e.getFileName() + ":" + e.getLineNumber() + " "; + Log.d(TAG, "invalidate: " + caller); + } + if (mRenderRequested) return; + mRenderRequested = true; + super.requestRender(); + } + + @Override + public void requestLayoutContentPane() { + mRenderLock.lock(); + try { + if (mContentView == null || (mFlags & FLAG_NEED_LAYOUT) != 0) return; + + // "View" system will invoke onLayout() for initialization(bug ?), we + // have to ignore it since the GLThread is not ready yet. + if ((mFlags & FLAG_INITIALIZED) == 0) return; + + mFlags |= FLAG_NEED_LAYOUT; + requestRender(); + } finally { + mRenderLock.unlock(); + } + } + + private void layoutContentPane() { + mFlags &= ~FLAG_NEED_LAYOUT; + int width = getWidth(); + int height = getHeight(); + Log.i(TAG, "layout content pane " + width + "x" + height); + if (mContentView != null && width != 0 && height != 0) { + mContentView.layout(0, 0, width, height); + } + // Uncomment this to dump the view hierarchy. + //mContentView.dumpTree(""); + } + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + if (changed) requestLayoutContentPane(); + } + + /** + * Called when the context is created, possibly after automatic destruction. + */ + // This is a GLSurfaceView.Renderer callback + @Override + public void onSurfaceCreated(GL10 gl1, EGLConfig config) { + GL11 gl = (GL11) gl1; + if (mGL != null) { + // The GL Object has changed + Log.i(TAG, "GLObject has changed from " + mGL + " to " + gl); + } + mGL = gl; + mCanvas = new GLCanvasImpl(gl); + if (!DEBUG_FPS) { + setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + } else { + setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); + } + } + + /** + * Called when the OpenGL surface is recreated without destroying the + * context. + */ + // This is a GLSurfaceView.Renderer callback + @Override + public void onSurfaceChanged(GL10 gl1, int width, int height) { + Log.i(TAG, "onSurfaceChanged: " + width + "x" + height + + ", gl10: " + gl1.toString()); + Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY); + GalleryUtils.setRenderThread(); + GL11 gl = (GL11) gl1; + Utils.assertTrue(mGL == gl); + + mCanvas.setSize(width, height); + + mClipRect.set(0, 0, width, height); + mClipRetryCount = 2; + } + + private void outputFps() { + long now = System.nanoTime(); + if (mFrameCountingStart == 0) { + mFrameCountingStart = now; + } else if ((now - mFrameCountingStart) > 1000000000) { + Log.d(TAG, "fps: " + (double) mFrameCount + * 1000000000 / (now - mFrameCountingStart)); + mFrameCountingStart = now; + mFrameCount = 0; + } + ++mFrameCount; + } + + @Override + public void onDrawFrame(GL10 gl) { + mRenderLock.lock(); + try { + onDrawFrameLocked(gl); + } finally { + mRenderLock.unlock(); + } + long end = SystemClock.uptimeMillis(); + + if (mLastDrawFinishTime != 0) { + long wait = mLastDrawFinishTime + TARGET_FRAME_TIME - end; + if (wait > 0) { + SystemClock.sleep(wait); + } + } + mLastDrawFinishTime = SystemClock.uptimeMillis(); + } + + private void onDrawFrameLocked(GL10 gl) { + if (DEBUG_FPS) outputFps(); + + // release the unbound textures and deleted buffers. + mCanvas.deleteRecycledResources(); + + // reset texture upload limit + UploadedTexture.resetUploadLimit(); + + mRenderRequested = false; + + if ((mFlags & FLAG_NEED_LAYOUT) != 0) layoutContentPane(); + + // OpenGL seems having a bug causing us not being able to reset the + // scissor box in "onSurfaceChanged()". We have to do it in the second + // onDrawFrame(). + if (mClipRetryCount > 0) { + --mClipRetryCount; + Rect clip = mClipRect; + gl.glScissor(clip.left, clip.top, clip.width(), clip.height()); + } + + mCanvas.setCurrentAnimationTimeMillis(SystemClock.uptimeMillis()); + if (mContentView != null) { + mContentView.render(mCanvas); + } + + if (!mAnimations.isEmpty()) { + long now = SystemClock.uptimeMillis(); + for (int i = 0, n = mAnimations.size(); i < n; i++) { + mAnimations.get(i).setStartTime(now); + } + mAnimations.clear(); + } + + if (UploadedTexture.uploadLimitReached()) { + requestRender(); + } + + synchronized (mIdleListeners) { + if (!mRenderRequested && !mIdleListeners.isEmpty()) { + mIdleRunner.enable(); + } + } + + if (DEBUG_INVALIDATE) { + mCanvas.fillRect(10, 10, 5, 5, mInvalidateColor); + mInvalidateColor = ~mInvalidateColor; + } + + if (DEBUG_DRAWING_STAT) { + mCanvas.dumpStatisticsAndClear(); + } + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + int action = event.getAction(); + if (action == MotionEvent.ACTION_CANCEL + || action == MotionEvent.ACTION_UP) { + mInDownState = false; + } else if (!mInDownState && action != MotionEvent.ACTION_DOWN) { + return false; + } + mRenderLock.lock(); + try { + // If this has been detached from root, we don't need to handle event + boolean handled = mContentView != null + && mContentView.dispatchTouchEvent(event); + if (action == MotionEvent.ACTION_DOWN && handled) { + mInDownState = true; + } + return handled; + } finally { + mRenderLock.unlock(); + } + } + + public DisplayMetrics getDisplayMetrics() { + if (mDisplayMetrics == null) { + mDisplayMetrics = new DisplayMetrics(); + ((Activity) getContext()).getWindowManager() + .getDefaultDisplay().getMetrics(mDisplayMetrics); + } + return mDisplayMetrics; + } + + public GLCanvas getCanvas() { + return mCanvas; + } + + private class IdleRunner implements Runnable { + // true if the idle runner is in the queue + private boolean mActive = false; + + @Override + public void run() { + OnGLIdleListener listener; + synchronized (mIdleListeners) { + mActive = false; + if (mRenderRequested) return; + if (mIdleListeners.isEmpty()) return; + listener = mIdleListeners.removeFirst(); + } + mRenderLock.lock(); + try { + if (!listener.onGLIdle(GLRootView.this, mCanvas)) return; + } finally { + mRenderLock.unlock(); + } + synchronized (mIdleListeners) { + mIdleListeners.addLast(listener); + enable(); + } + } + + public void enable() { + // Who gets the flag can add it to the queue + if (mActive) return; + mActive = true; + queueEvent(this); + } + } + + @Override + public void lockRenderThread() { + mRenderLock.lock(); + } + + @Override + public void unlockRenderThread() { + mRenderLock.unlock(); + } +} diff --git a/src/com/android/gallery3d/ui/GLView.java b/src/com/android/gallery3d/ui/GLView.java new file mode 100644 index 000000000..c59327831 --- /dev/null +++ b/src/com/android/gallery3d/ui/GLView.java @@ -0,0 +1,431 @@ +/* + * 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.ui; + +import com.android.gallery3d.anim.CanvasAnimation; +import com.android.gallery3d.common.Utils; + +import android.graphics.Rect; +import android.os.SystemClock; +import android.view.MotionEvent; + +import java.util.ArrayList; + +// GLView is a UI component. It can render to a GLCanvas and accept touch +// events. A GLView may have zero or more child GLView and they form a tree +// structure. The rendering and event handling will pass through the tree +// structure. +// +// A GLView tree should be attached to a GLRoot before event dispatching and +// rendering happens. GLView asks GLRoot to re-render or re-layout the +// GLView hierarchy using requestRender() and requestLayoutContentPane(). +// +// The render() method is called in a separate thread. Before calling +// dispatchTouchEvent() and layout(), GLRoot acquires a lock to avoid the +// rendering thread running at the same time. If there are other entry points +// from main thread (like a Handler) in your GLView, you need to call +// lockRendering() if the rendering thread should not run at the same time. +// +public class GLView { + private static final String TAG = "GLView"; + + public static final int VISIBLE = 0; + public static final int INVISIBLE = 1; + + private static final int FLAG_INVISIBLE = 1; + private static final int FLAG_SET_MEASURED_SIZE = 2; + private static final int FLAG_LAYOUT_REQUESTED = 4; + + protected final Rect mBounds = new Rect(); + protected final Rect mPaddings = new Rect(); + + private GLRoot mRoot; + protected GLView mParent; + private ArrayList<GLView> mComponents; + private GLView mMotionTarget; + + private CanvasAnimation mAnimation; + + private int mViewFlags = 0; + + protected int mMeasuredWidth = 0; + protected int mMeasuredHeight = 0; + + private int mLastWidthSpec = -1; + private int mLastHeightSpec = -1; + + protected int mScrollY = 0; + protected int mScrollX = 0; + protected int mScrollHeight = 0; + protected int mScrollWidth = 0; + + public void startAnimation(CanvasAnimation animation) { + GLRoot root = getGLRoot(); + if (root == null) throw new IllegalStateException(); + + mAnimation = animation; + mAnimation.start(); + root.registerLaunchedAnimation(mAnimation); + invalidate(); + } + + // Sets the visiblity of this GLView (either GLView.VISIBLE or + // GLView.INVISIBLE). + public void setVisibility(int visibility) { + if (visibility == getVisibility()) return; + if (visibility == VISIBLE) { + mViewFlags &= ~FLAG_INVISIBLE; + } else { + mViewFlags |= FLAG_INVISIBLE; + } + onVisibilityChanged(visibility); + invalidate(); + } + + // Returns GLView.VISIBLE or GLView.INVISIBLE + public int getVisibility() { + return (mViewFlags & FLAG_INVISIBLE) == 0 ? VISIBLE : INVISIBLE; + } + + // This should only be called on the content pane (the topmost GLView). + public void attachToRoot(GLRoot root) { + Utils.assertTrue(mParent == null && mRoot == null); + onAttachToRoot(root); + } + + // This should only be called on the content pane (the topmost GLView). + public void detachFromRoot() { + Utils.assertTrue(mParent == null && mRoot != null); + onDetachFromRoot(); + } + + // Returns the number of children of the GLView. + public int getComponentCount() { + return mComponents == null ? 0 : mComponents.size(); + } + + // Returns the children for the given index. + public GLView getComponent(int index) { + if (mComponents == null) { + throw new ArrayIndexOutOfBoundsException(index); + } + return mComponents.get(index); + } + + // Adds a child to this GLView. + public void addComponent(GLView component) { + // Make sure the component doesn't have a parent currently. + if (component.mParent != null) throw new IllegalStateException(); + + // Build parent-child links + if (mComponents == null) { + mComponents = new ArrayList<GLView>(); + } + mComponents.add(component); + component.mParent = this; + + // If this is added after we have a root, tell the component. + if (mRoot != null) { + component.onAttachToRoot(mRoot); + } + } + + // Removes a child from this GLView. + public boolean removeComponent(GLView component) { + if (mComponents == null) return false; + if (mComponents.remove(component)) { + removeOneComponent(component); + return true; + } + return false; + } + + // Removes all children of this GLView. + public void removeAllComponents() { + for (int i = 0, n = mComponents.size(); i < n; ++i) { + removeOneComponent(mComponents.get(i)); + } + mComponents.clear(); + } + + private void removeOneComponent(GLView component) { + if (mMotionTarget == component) { + long now = SystemClock.uptimeMillis(); + MotionEvent cancelEvent = MotionEvent.obtain( + now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); + dispatchTouchEvent(cancelEvent); + cancelEvent.recycle(); + } + component.onDetachFromRoot(); + component.mParent = null; + } + + public Rect bounds() { + return mBounds; + } + + public int getWidth() { + return mBounds.right - mBounds.left; + } + + public int getHeight() { + return mBounds.bottom - mBounds.top; + } + + public GLRoot getGLRoot() { + return mRoot; + } + + // Request re-rendering of the view hierarchy. + // This is used for animation or when the contents changed. + public void invalidate() { + GLRoot root = getGLRoot(); + if (root != null) root.requestRender(); + } + + // Request re-layout of the view hierarchy. + public void requestLayout() { + mViewFlags |= FLAG_LAYOUT_REQUESTED; + mLastHeightSpec = -1; + mLastWidthSpec = -1; + if (mParent != null) { + mParent.requestLayout(); + } else { + // Is this a content pane ? + GLRoot root = getGLRoot(); + if (root != null) root.requestLayoutContentPane(); + } + } + + protected void render(GLCanvas canvas) { + renderBackground(canvas); + for (int i = 0, n = getComponentCount(); i < n; ++i) { + renderChild(canvas, getComponent(i)); + } + } + + protected void renderBackground(GLCanvas view) { + } + + protected void renderChild(GLCanvas canvas, GLView component) { + if (component.getVisibility() != GLView.VISIBLE + && component.mAnimation == null) return; + + int xoffset = component.mBounds.left - mScrollX; + int yoffset = component.mBounds.top - mScrollY; + + canvas.translate(xoffset, yoffset, 0); + + CanvasAnimation anim = component.mAnimation; + if (anim != null) { + canvas.save(anim.getCanvasSaveFlags()); + if (anim.calculate(canvas.currentAnimationTimeMillis())) { + invalidate(); + } else { + component.mAnimation = null; + } + anim.apply(canvas); + } + component.render(canvas); + if (anim != null) canvas.restore(); + canvas.translate(-xoffset, -yoffset, 0); + } + + protected boolean onTouch(MotionEvent event) { + return false; + } + + protected boolean dispatchTouchEvent(MotionEvent event, + int x, int y, GLView component, boolean checkBounds) { + Rect rect = component.mBounds; + int left = rect.left; + int top = rect.top; + if (!checkBounds || rect.contains(x, y)) { + event.offsetLocation(-left, -top); + if (component.dispatchTouchEvent(event)) { + event.offsetLocation(left, top); + return true; + } + event.offsetLocation(left, top); + } + return false; + } + + protected boolean dispatchTouchEvent(MotionEvent event) { + int x = (int) event.getX(); + int y = (int) event.getY(); + int action = event.getAction(); + if (mMotionTarget != null) { + if (action == MotionEvent.ACTION_DOWN) { + MotionEvent cancel = MotionEvent.obtain(event); + cancel.setAction(MotionEvent.ACTION_CANCEL); + dispatchTouchEvent(cancel, x, y, mMotionTarget, false); + mMotionTarget = null; + } else { + dispatchTouchEvent(event, x, y, mMotionTarget, false); + if (action == MotionEvent.ACTION_CANCEL + || action == MotionEvent.ACTION_UP) { + mMotionTarget = null; + } + return true; + } + } + if (action == MotionEvent.ACTION_DOWN) { + // in the reverse rendering order + for (int i = getComponentCount() - 1; i >= 0; --i) { + GLView component = getComponent(i); + if (component.getVisibility() != GLView.VISIBLE) continue; + if (dispatchTouchEvent(event, x, y, component, true)) { + mMotionTarget = component; + return true; + } + } + } + return onTouch(event); + } + + public Rect getPaddings() { + return mPaddings; + } + + public void setPaddings(Rect paddings) { + mPaddings.set(paddings); + } + + public void setPaddings(int left, int top, int right, int bottom) { + mPaddings.set(left, top, right, bottom); + } + + public void layout(int left, int top, int right, int bottom) { + boolean sizeChanged = setBounds(left, top, right, bottom); + if (sizeChanged) { + mViewFlags &= ~FLAG_LAYOUT_REQUESTED; + onLayout(true, left, top, right, bottom); + } else if ((mViewFlags & FLAG_LAYOUT_REQUESTED)!= 0) { + mViewFlags &= ~FLAG_LAYOUT_REQUESTED; + onLayout(false, left, top, right, bottom); + } + } + + private boolean setBounds(int left, int top, int right, int bottom) { + boolean sizeChanged = (right - left) != (mBounds.right - mBounds.left) + || (bottom - top) != (mBounds.bottom - mBounds.top); + mBounds.set(left, top, right, bottom); + return sizeChanged; + } + + public void measure(int widthSpec, int heightSpec) { + if (widthSpec == mLastWidthSpec && heightSpec == mLastHeightSpec + && (mViewFlags & FLAG_LAYOUT_REQUESTED) == 0) { + return; + } + + mLastWidthSpec = widthSpec; + mLastHeightSpec = heightSpec; + + mViewFlags &= ~FLAG_SET_MEASURED_SIZE; + onMeasure(widthSpec, heightSpec); + if ((mViewFlags & FLAG_SET_MEASURED_SIZE) == 0) { + throw new IllegalStateException(getClass().getName() + + " should call setMeasuredSize() in onMeasure()"); + } + } + + protected void onMeasure(int widthSpec, int heightSpec) { + } + + protected void setMeasuredSize(int width, int height) { + mViewFlags |= FLAG_SET_MEASURED_SIZE; + mMeasuredWidth = width; + mMeasuredHeight = height; + } + + public int getMeasuredWidth() { + return mMeasuredWidth; + } + + public int getMeasuredHeight() { + return mMeasuredHeight; + } + + protected void onLayout( + boolean changeSize, int left, int top, int right, int bottom) { + } + + /** + * Gets the bounds of the given descendant that relative to this view. + */ + public boolean getBoundsOf(GLView descendant, Rect out) { + int xoffset = 0; + int yoffset = 0; + GLView view = descendant; + while (view != this) { + if (view == null) return false; + Rect bounds = view.mBounds; + xoffset += bounds.left; + yoffset += bounds.top; + view = view.mParent; + } + out.set(xoffset, yoffset, xoffset + descendant.getWidth(), + yoffset + descendant.getHeight()); + return true; + } + + protected void onVisibilityChanged(int visibility) { + for (int i = 0, n = getComponentCount(); i < n; ++i) { + GLView child = getComponent(i); + if (child.getVisibility() == GLView.VISIBLE) { + child.onVisibilityChanged(visibility); + } + } + } + + protected void onAttachToRoot(GLRoot root) { + mRoot = root; + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).onAttachToRoot(root); + } + } + + protected void onDetachFromRoot() { + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).onDetachFromRoot(); + } + mRoot = null; + } + + public void lockRendering() { + if (mRoot != null) { + mRoot.lockRenderThread(); + } + } + + public void unlockRendering() { + if (mRoot != null) { + mRoot.unlockRenderThread(); + } + } + + // This is for debugging only. + // Dump the view hierarchy into log. + void dumpTree(String prefix) { + Log.d(TAG, prefix + getClass().getSimpleName()); + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).dumpTree(prefix + "...."); + } + } +} diff --git a/src/com/android/gallery3d/ui/GalleryEGLConfigChooser.java b/src/com/android/gallery3d/ui/GalleryEGLConfigChooser.java new file mode 100644 index 000000000..1d50d43f7 --- /dev/null +++ b/src/com/android/gallery3d/ui/GalleryEGLConfigChooser.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2009 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.ui; + +import android.opengl.GLSurfaceView.EGLConfigChooser; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLDisplay; + +/* + * The code is copied/adapted from + * <code>android.opengl.GLSurfaceView.BaseConfigChooser</code>. Here we try to + * choose a configuration that support RGBA_8888 format and if possible, + * with stencil buffer, but is not required. + */ +class GalleryEGLConfigChooser implements EGLConfigChooser { + + private static final String TAG = "GalleryEGLConfigChooser"; + private int mStencilBits; + + private final int mConfigSpec[] = new int[] { + EGL10.EGL_RED_SIZE, 5, + EGL10.EGL_GREEN_SIZE, 6, + EGL10.EGL_BLUE_SIZE, 5, + EGL10.EGL_ALPHA_SIZE, 0, + EGL10.EGL_NONE + }; + + public int getStencilBits() { + return mStencilBits; + } + + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + int[] numConfig = new int[1]; + if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, numConfig)) { + throw new RuntimeException("eglChooseConfig failed"); + } + + if (numConfig[0] <= 0) { + throw new RuntimeException("No configs match configSpec"); + } + + EGLConfig[] configs = new EGLConfig[numConfig[0]]; + if (!egl.eglChooseConfig(display, + mConfigSpec, configs, configs.length, numConfig)) { + throw new RuntimeException(); + } + + return chooseConfig(egl, display, configs); + } + + private EGLConfig chooseConfig( + EGL10 egl, EGLDisplay display, EGLConfig configs[]) { + + EGLConfig result = null; + int minStencil = Integer.MAX_VALUE; + int value[] = new int[1]; + + // Because we need only one bit of stencil, try to choose a config that + // has stencil support but with smallest number of stencil bits. If + // none is found, choose any one. + for (int i = 0, n = configs.length; i < n; ++i) { + if (egl.eglGetConfigAttrib( + display, configs[i], EGL10.EGL_RED_SIZE, value)) { + // Filter out ARGB 8888 configs. + if (value[0] == 8) continue; + } + if (egl.eglGetConfigAttrib( + display, configs[i], EGL10.EGL_STENCIL_SIZE, value)) { + if (value[0] == 0) continue; + if (value[0] < minStencil) { + minStencil = value[0]; + result = configs[i]; + } + } else { + throw new RuntimeException( + "eglGetConfigAttrib error: " + egl.eglGetError()); + } + } + if (result == null) result = configs[0]; + egl.eglGetConfigAttrib( + display, result, EGL10.EGL_STENCIL_SIZE, value); + mStencilBits = value[0]; + logConfig(egl, display, result); + return result; + } + + private static final int[] ATTR_ID = { + EGL10.EGL_RED_SIZE, + EGL10.EGL_GREEN_SIZE, + EGL10.EGL_BLUE_SIZE, + EGL10.EGL_ALPHA_SIZE, + EGL10.EGL_DEPTH_SIZE, + EGL10.EGL_STENCIL_SIZE, + EGL10.EGL_CONFIG_ID, + EGL10.EGL_CONFIG_CAVEAT + }; + + private static final String[] ATTR_NAME = { + "R", "G", "B", "A", "D", "S", "ID", "CAVEAT" + }; + + private void logConfig(EGL10 egl, EGLDisplay display, EGLConfig config) { + int value[] = new int[1]; + StringBuilder sb = new StringBuilder(); + for (int j = 0; j < ATTR_ID.length; j++) { + egl.eglGetConfigAttrib(display, config, ATTR_ID[j], value); + sb.append(ATTR_NAME[j] + value[0] + " "); + } + Log.i(TAG, "Config chosen: " + sb.toString()); + } +} diff --git a/src/com/android/gallery3d/ui/GridDrawer.java b/src/com/android/gallery3d/ui/GridDrawer.java new file mode 100644 index 000000000..54b175cb4 --- /dev/null +++ b/src/com/android/gallery3d/ui/GridDrawer.java @@ -0,0 +1,109 @@ +/* + * 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.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.Path; + +import android.content.Context; +import android.graphics.Color; + +public class GridDrawer extends IconDrawer { + private final NinePatchTexture mFrame; + private final NinePatchTexture mFrameSelected; + private final NinePatchTexture mFrameSelectedTop; + private final NinePatchTexture mImportBackground; + private Texture mImportLabel; + private int mGridWidth; + private final SelectionManager mSelectionManager; + private final Context mContext; + private final int FONT_SIZE = 14; + private final int FONT_COLOR = Color.WHITE; + private final int IMPORT_LABEL_PADDING = 10; + private boolean mSelectionMode; + + public GridDrawer(Context context, SelectionManager selectionManager) { + super(context); + mContext = context; + mFrame = new NinePatchTexture(context, R.drawable.album_frame); + mFrameSelected = new NinePatchTexture(context, R.drawable.grid_selected); + mFrameSelectedTop = new NinePatchTexture(context, R.drawable.grid_selected_top); + mImportBackground = new NinePatchTexture(context, R.drawable.import_translucent); + mSelectionManager = selectionManager; + } + + @Override + public void prepareDrawing() { + mSelectionMode = mSelectionManager.inSelectionMode(); + } + + @Override + public void draw(GLCanvas canvas, Texture content, int width, int height, + int rotation, Path path, int topIndex, int dataSourceType, + int mediaType, boolean wantCache, boolean isCaching) { + + int x = -width / 2; + int y = -height / 2; + + drawWithRotationAndGray(canvas, content, x, y, width, height, rotation, + topIndex); + + if (((rotation / 90) & 0x01) == 1) { + int temp = width; + width = height; + height = temp; + x = -width / 2; + y = -height / 2; + } + + drawVideoOverlay(canvas, mediaType, x, y, width, height, topIndex); + + NinePatchTexture frame; + if (mSelectionMode && mSelectionManager.isItemSelected(path)) { + frame = topIndex == 0 ? mFrameSelectedTop : mFrameSelected; + } else { + frame = mFrame; + } + + drawFrame(canvas, frame, x, y, width, height); + + if (topIndex == 0) { + ResourceTexture icon = getIcon(dataSourceType); + if (icon != null) { + IconDimension id = getIconDimension(icon, width, height); + if (dataSourceType == DATASOURCE_TYPE_MTP) { + if (mImportLabel == null || mGridWidth != width) { + mGridWidth = width; + mImportLabel = MultiLineTexture.newInstance( + mContext.getString(R.string.click_import), + width - id.width - IMPORT_LABEL_PADDING, FONT_SIZE, FONT_COLOR); + } + int bgHeight = Math.max(id.height, mImportLabel.getHeight()); + mImportBackground.setSize(width, bgHeight); + mImportBackground.draw(canvas, x, -y - bgHeight); + mImportLabel.draw(canvas, x + id.width + IMPORT_LABEL_PADDING, + -y - bgHeight + Math.abs(bgHeight - mImportLabel.getHeight()) / 2); + } + icon.draw(canvas, id.x, id.y, id.width, id.height); + } + } + } + + @Override + public void drawFocus(GLCanvas canvas, int width, int height) { + } +} diff --git a/src/com/android/gallery3d/ui/HighlightDrawer.java b/src/com/android/gallery3d/ui/HighlightDrawer.java new file mode 100644 index 000000000..9d5868bcb --- /dev/null +++ b/src/com/android/gallery3d/ui/HighlightDrawer.java @@ -0,0 +1,73 @@ +/* + * 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.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.Path; + +import android.content.Context; + +public class HighlightDrawer extends IconDrawer { + private final NinePatchTexture mFrame; + private final NinePatchTexture mFrameSelected; + private final NinePatchTexture mFrameSelectedTop; + private SelectionManager mSelectionManager; + private Path mHighlightItem; + + public HighlightDrawer(Context context) { + super(context); + mFrame = new NinePatchTexture(context, R.drawable.album_frame); + mFrameSelected = new NinePatchTexture(context, R.drawable.grid_selected); + mFrameSelectedTop = new NinePatchTexture(context, R.drawable.grid_selected_top); + } + + public void setHighlightItem(Path item) { + mHighlightItem = item; + } + + public void draw(GLCanvas canvas, Texture content, int width, int height, + int rotation, Path path, int topIndex, int dataSourceType, + int mediaType, boolean wantCache, boolean isCaching) { + int x = -width / 2; + int y = -height / 2; + + drawWithRotationAndGray(canvas, content, x, y, width, height, rotation, + topIndex); + + if (((rotation / 90) & 0x01) == 1) { + int temp = width; + width = height; + height = temp; + x = -width / 2; + y = -height / 2; + } + + drawVideoOverlay(canvas, mediaType, x, y, width, height, topIndex); + + NinePatchTexture frame; + if (path == mHighlightItem) { + frame = topIndex == 0 ? mFrameSelectedTop : mFrameSelected; + } else { + frame = mFrame; + } + + drawFrame(canvas, frame, x, y, width, height); + + if (topIndex == 0) { + drawIcon(canvas, width, height, dataSourceType); + } + } +} diff --git a/src/com/android/gallery3d/ui/Icon.java b/src/com/android/gallery3d/ui/Icon.java new file mode 100644 index 000000000..c710859f8 --- /dev/null +++ b/src/com/android/gallery3d/ui/Icon.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.ui; + +import android.content.Context; +import android.graphics.Rect; + +public class Icon extends GLView { + private final BasicTexture mIcon; + + // The width and height requested by the user. + private int mReqWidth; + private int mReqHeight; + + public Icon(Context context, int iconId, int width, int height) { + this(context, new ResourceTexture(context, iconId), width, height); + } + + public Icon(Context context, BasicTexture icon, int width, int height) { + mIcon = icon; + mReqWidth = width; + mReqHeight = height; + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + MeasureHelper.getInstance(this) + .setPreferredContentSize(mReqWidth, mReqHeight) + .measure(widthSpec, heightSpec); + } + + @Override + protected void render(GLCanvas canvas) { + Rect p = mPaddings; + + int width = getWidth() - p.left - p.right; + int height = getHeight() - p.top - p.bottom; + + // Draw the icon in the center of the space + int xoffset = p.left + (width - mReqWidth) / 2; + int yoffset = p.top + (height - mReqHeight) / 2; + + mIcon.draw(canvas, xoffset, yoffset, mReqWidth, mReqHeight); + } +} diff --git a/src/com/android/gallery3d/ui/IconDrawer.java b/src/com/android/gallery3d/ui/IconDrawer.java new file mode 100644 index 000000000..91732d338 --- /dev/null +++ b/src/com/android/gallery3d/ui/IconDrawer.java @@ -0,0 +1,112 @@ +/* + * 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.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.MediaObject; + +import android.content.Context; + +public abstract class IconDrawer extends SelectionDrawer { + private final String TAG = "IconDrawer"; + private final ResourceTexture mLocalSetIcon; + private final ResourceTexture mCameraIcon; + private final ResourceTexture mPicasaIcon; + private final ResourceTexture mMtpIcon; + private final Texture mVideoOverlay; + private final Texture mVideoPlayIcon; + + public static class IconDimension { + int x; + int y; + int width; + int height; + } + + public IconDrawer(Context context) { + mLocalSetIcon = new ResourceTexture(context, R.drawable.ic_album_overlay_folder_holo); + mCameraIcon = new ResourceTexture(context, R.drawable.ic_album_overlay_camera_holo); + mPicasaIcon = new ResourceTexture(context, R.drawable.ic_album_overlay_picassa_holo); + mMtpIcon = new ResourceTexture(context, R.drawable.ic_album_overlay_ptp_holo); + mVideoOverlay = new ResourceTexture(context, + R.drawable.thumbnail_album_video_overlay_holo); + mVideoPlayIcon = new ResourceTexture(context, + R.drawable.videooverlay); + } + + @Override + public void prepareDrawing() { + } + + protected IconDimension drawIcon(GLCanvas canvas, int width, int height, + int dataSourceType) { + ResourceTexture icon = getIcon(dataSourceType); + + if (icon != null) { + IconDimension id = getIconDimension(icon, width, height); + icon.draw(canvas, id.x, id.y, id.width, id.height); + return id; + } + return null; + } + + protected ResourceTexture getIcon(int dataSourceType) { + ResourceTexture icon = null; + switch (dataSourceType) { + case DATASOURCE_TYPE_LOCAL: + icon = mLocalSetIcon; + break; + case DATASOURCE_TYPE_PICASA: + icon = mPicasaIcon; + break; + case DATASOURCE_TYPE_CAMERA: + icon = mCameraIcon; + break; + case DATASOURCE_TYPE_MTP: + icon = mMtpIcon; + break; + default: + break; + } + + return icon; + } + + protected IconDimension getIconDimension(ResourceTexture icon, int width, + int height) { + IconDimension id = new IconDimension(); + float scale = 0.25f * width / icon.getWidth(); + id.width = (int) (scale * icon.getWidth()); + id.height = (int) (scale * icon.getHeight()); + id.x = -width / 2; + id.y = height / 2 - id.height; + return id; + } + + protected void drawVideoOverlay(GLCanvas canvas, int mediaType, + int x, int y, int width, int height, int topIndex) { + if (mediaType != MediaObject.MEDIA_TYPE_VIDEO) return; + mVideoOverlay.draw(canvas, x, y, width, height); + if (topIndex == 0) { + int side = Math.min(width, height) / 6; + mVideoPlayIcon.draw(canvas, -side / 2, -side / 2, side, side); + } + } + + @Override + public void drawFocus(GLCanvas canvas, int width, int height) { + } +} diff --git a/src/com/android/gallery3d/ui/ImportCompleteListener.java b/src/com/android/gallery3d/ui/ImportCompleteListener.java new file mode 100644 index 000000000..5c52ea135 --- /dev/null +++ b/src/com/android/gallery3d/ui/ImportCompleteListener.java @@ -0,0 +1,57 @@ +/* + * 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.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AlbumPage; +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.util.MediaSetUtils; + +import android.content.Context; +import android.os.Bundle; +import android.widget.Toast; + +public class ImportCompleteListener implements MenuExecutor.ProgressListener { + private GalleryActivity mActivity; + + public ImportCompleteListener(GalleryActivity galleryActivity) { + mActivity = galleryActivity; + } + + public void onProgressComplete(int result) { + int message; + if (result == MenuExecutor.EXECUTION_RESULT_SUCCESS) { + message = R.string.import_complete; + goToImportedAlbum(); + } else { + message = R.string.import_fail; + } + Toast.makeText(mActivity.getAndroidContext(), message, Toast.LENGTH_LONG).show(); + } + + public void onProgressUpdate(int index) { + } + + private void goToImportedAlbum() { + String pathOfImportedAlbum = "/local/all/" + MediaSetUtils.IMPORTED_BUCKET_ID; + Bundle data = new Bundle(); + data.putString(AlbumPage.KEY_MEDIA_PATH, pathOfImportedAlbum); + mActivity.getStateManager().startState(AlbumPage.class, data); + } + +} diff --git a/src/com/android/gallery3d/ui/Label.java b/src/com/android/gallery3d/ui/Label.java new file mode 100644 index 000000000..6a70a1895 --- /dev/null +++ b/src/com/android/gallery3d/ui/Label.java @@ -0,0 +1,84 @@ +/* + * 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.ui; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Rect; + +public class Label extends GLView { + private static final String TAG = "Label"; + public static final int NULL_ID = 0; + + private static final int FONT_SIZE = 18; + private static final int FONT_COLOR = Color.WHITE; + + private String mText; + private StringTexture mTexture; + private int mFontSize, mFontColor; + + public Label(Context context, int stringId, + int fontSize, int fontColor) { + this(context, context.getString(stringId), fontSize, fontColor); + } + + public Label(Context context, int stringId) { + this(context, stringId, FONT_SIZE, FONT_COLOR); + } + + public Label(Context context, String text) { + this(context, text, FONT_SIZE, FONT_COLOR); + } + + public Label(Context context, String text, int fontSize, int fontColor) { + //TODO: cut the text if it is too long + mText = text; + mTexture = StringTexture.newInstance(text, fontSize, fontColor); + mFontSize = fontSize; + mFontColor = fontColor; + } + + public void setText(String text) { + if (!mText.equals(text)) { + mText = text; + mTexture = StringTexture.newInstance(text, mFontSize, mFontColor); + requestLayout(); + } + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + int width = mTexture.getWidth(); + int height = mTexture.getHeight(); + MeasureHelper.getInstance(this) + .setPreferredContentSize(width, height) + .measure(widthSpec, heightSpec); + } + + @Override + protected void render(GLCanvas canvas) { + Rect p = mPaddings; + + int width = getWidth() - p.left - p.right; + int height = getHeight() - p.top - p.bottom; + + int xoffset = p.left + (width - mTexture.getWidth()) / 2; + int yoffset = p.top + (height - mTexture.getHeight()) / 2; + + mTexture.draw(canvas, xoffset, yoffset); + } +} diff --git a/src/com/android/gallery3d/ui/Log.java b/src/com/android/gallery3d/ui/Log.java new file mode 100644 index 000000000..32adc98eb --- /dev/null +++ b/src/com/android/gallery3d/ui/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.ui; + +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/ui/ManageCacheDrawer.java b/src/com/android/gallery3d/ui/ManageCacheDrawer.java new file mode 100644 index 000000000..cf1e39e24 --- /dev/null +++ b/src/com/android/gallery3d/ui/ManageCacheDrawer.java @@ -0,0 +1,126 @@ +/* + * 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.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.Path; + +import android.content.Context; + +public class ManageCacheDrawer extends IconDrawer { + private static final int COLOR_CACHING_BACKGROUND = 0x7F000000; + private static final int ICON_SIZE = 36; + private final NinePatchTexture mFrame; + private final ResourceTexture mCheckedItem; + private final ResourceTexture mUnCheckedItem; + private final SelectionManager mSelectionManager; + + private final ResourceTexture mLocalAlbumIcon; + private final StringTexture mCaching; + + public ManageCacheDrawer(Context context, SelectionManager selectionManager) { + super(context); + mFrame = new NinePatchTexture(context, R.drawable.manage_frame); + mCheckedItem = new ResourceTexture(context, R.drawable.btn_make_offline_normal_on_holo_dark); + mUnCheckedItem = new ResourceTexture(context, R.drawable.btn_make_offline_normal_off_holo_dark); + mLocalAlbumIcon = new ResourceTexture(context, R.drawable.btn_make_offline_disabled_on_holo_dark); + String cachingLabel = context.getString(R.string.caching_label); + mCaching = StringTexture.newInstance(cachingLabel, 12, 0xffffffff); + mSelectionManager = selectionManager; + } + + @Override + public void prepareDrawing() { + } + + private static boolean isLocal(int dataSourceType) { + return dataSourceType != DATASOURCE_TYPE_PICASA; + } + + @Override + public void draw(GLCanvas canvas, Texture content, int width, int height, + int rotation, Path path, int topIndex, int dataSourceType, + int mediaType, boolean wantCache, boolean isCaching) { + + boolean selected = mSelectionManager.isItemSelected(path); + boolean chooseToCache = wantCache ^ selected; + + int x = -width / 2; + int y = -height / 2; + + drawWithRotationAndGray(canvas, content, x, y, width, height, rotation, + topIndex); + + if (((rotation / 90) & 0x01) == 1) { + int temp = width; + width = height; + height = temp; + x = -width / 2; + y = -height / 2; + } + + drawVideoOverlay(canvas, mediaType, x, y, width, height, topIndex); + + drawFrame(canvas, mFrame, x, y, width, height); + + if (topIndex == 0) { + drawIcon(canvas, width, height, dataSourceType); + } + + if (topIndex == 0) { + ResourceTexture icon = null; + if (isLocal(dataSourceType)) { + icon = mLocalAlbumIcon; + } else if (chooseToCache) { + icon = mCheckedItem; + } else { + icon = mUnCheckedItem; + } + + int w = ICON_SIZE; + int h = ICON_SIZE; + x = width / 2 - w / 2; + y = -height / 2 - h / 2; + + icon.draw(canvas, x, y, w, h); + + if (isCaching) { + int textWidth = mCaching.getWidth(); + int textHeight = mCaching.getHeight(); + x = -textWidth / 2; + y = height / 2 - textHeight; + + // Leave a few pixels of margin in the background rect. + float sideMargin = Utils.clamp(textWidth * 0.1f, 2.0f, + 6.0f); + float clearance = Utils.clamp(textHeight * 0.1f, 2.0f, + 6.0f); + + // Overlay the "Caching" wording at the bottom-center of the content. + canvas.fillRect(x - sideMargin, y - clearance, + textWidth + sideMargin * 2, textHeight + clearance, + COLOR_CACHING_BACKGROUND); + mCaching.draw(canvas, x, y); + } + } + } + + @Override + public void drawFocus(GLCanvas canvas, int width, int height) { + } +} diff --git a/src/com/android/gallery3d/ui/MeasureHelper.java b/src/com/android/gallery3d/ui/MeasureHelper.java new file mode 100644 index 000000000..f65dc10b3 --- /dev/null +++ b/src/com/android/gallery3d/ui/MeasureHelper.java @@ -0,0 +1,65 @@ +/* + * 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.ui; + +import android.graphics.Rect; +import android.view.View.MeasureSpec; + +class MeasureHelper { + + private static MeasureHelper sInstance = new MeasureHelper(null); + + private GLView mComponent; + private int mPreferredWidth; + private int mPreferredHeight; + + private MeasureHelper(GLView component) { + mComponent = component; + } + + public static MeasureHelper getInstance(GLView component) { + sInstance.mComponent = component; + return sInstance; + } + + public MeasureHelper setPreferredContentSize(int width, int height) { + mPreferredWidth = width; + mPreferredHeight = height; + return this; + } + + public void measure(int widthSpec, int heightSpec) { + Rect p = mComponent.getPaddings(); + setMeasuredSize( + getLength(widthSpec, mPreferredWidth + p.left + p.right), + getLength(heightSpec, mPreferredHeight + p.top + p.bottom)); + } + + private static int getLength(int measureSpec, int prefered) { + int specLength = MeasureSpec.getSize(measureSpec); + switch(MeasureSpec.getMode(measureSpec)) { + case MeasureSpec.EXACTLY: return specLength; + case MeasureSpec.AT_MOST: return Math.min(prefered, specLength); + default: return prefered; + } + } + + protected void setMeasuredSize(int width, int height) { + mComponent.setMeasuredSize(width, height); + } + +} diff --git a/src/com/android/gallery3d/ui/MenuExecutor.java b/src/com/android/gallery3d/ui/MenuExecutor.java new file mode 100644 index 000000000..710ddc422 --- /dev/null +++ b/src/com/android/gallery3d/ui/MenuExecutor.java @@ -0,0 +1,398 @@ +/* + * 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.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.CropImage; +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Handler; +import android.os.Message; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.Toast; + +import java.util.ArrayList; + +public class MenuExecutor { + @SuppressWarnings("unused") + private static final String TAG = "MenuExecutor"; + + private static final int MSG_TASK_COMPLETE = 1; + private static final int MSG_TASK_UPDATE = 2; + private static final int MSG_DO_SHARE = 3; + + public static final int EXECUTION_RESULT_SUCCESS = 1; + public static final int EXECUTION_RESULT_FAIL = 2; + public static final int EXECUTION_RESULT_CANCEL = 3; + + private ProgressDialog mDialog; + private Future<?> mTask; + + private final GalleryActivity mActivity; + private final SelectionManager mSelectionManager; + private final Handler mHandler; + + private static ProgressDialog showProgressDialog( + Context context, int titleId, int progressMax) { + ProgressDialog dialog = new ProgressDialog(context); + dialog.setTitle(titleId); + dialog.setMax(progressMax); + dialog.setCancelable(false); + dialog.setIndeterminate(false); + if (progressMax > 1) { + dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + } + dialog.show(); + return dialog; + } + + public interface ProgressListener { + public void onProgressUpdate(int index); + public void onProgressComplete(int result); + } + + public MenuExecutor( + GalleryActivity activity, SelectionManager selectionManager) { + mActivity = Utils.checkNotNull(activity); + mSelectionManager = Utils.checkNotNull(selectionManager); + mHandler = new SynchronizedHandler(mActivity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_TASK_COMPLETE: { + if (mDialog != null) { + mDialog.dismiss(); + mDialog = null; + mTask = null; + } + if (message.obj != null) { + ProgressListener listener = (ProgressListener) message.obj; + listener.onProgressComplete(message.arg1); + } + mSelectionManager.leaveSelectionMode(); + break; + } + case MSG_TASK_UPDATE: { + if (mDialog != null) mDialog.setProgress(message.arg1); + if (message.obj != null) { + ProgressListener listener = (ProgressListener) message.obj; + listener.onProgressUpdate(message.arg1); + } + break; + } + case MSG_DO_SHARE: { + ((Activity) mActivity).startActivity((Intent) message.obj); + break; + } + } + } + }; + } + + public void pause() { + if (mTask != null) { + mTask.cancel(); + mTask.waitDone(); + mDialog.dismiss(); + mDialog = null; + mTask = null; + } + } + + private void onProgressUpdate(int index, ProgressListener listener) { + mHandler.sendMessage( + mHandler.obtainMessage(MSG_TASK_UPDATE, index, 0, listener)); + } + + private void onProgressComplete(int result, ProgressListener listener) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_TASK_COMPLETE, result, 0, listener)); + } + + private int getShareType(SelectionManager selectionManager) { + ArrayList<Path> items = selectionManager.getSelected(false); + int type = 0; + DataManager dataManager = mActivity.getDataManager(); + for (Path id : items) { + type |= dataManager.getMediaType(id); + } + return type; + } + + private void onShareItemClicked(final SelectionManager selectionManager, + final String mimeType, final ComponentName component) { + Utils.assertTrue(mDialog == null); + final ArrayList<Path> items = selectionManager.getSelected(true); + mDialog = showProgressDialog((Activity) mActivity, + R.string.loading_image, items.size()); + + mTask = mActivity.getThreadPool().submit(new Job<Void>() { + @Override + public Void run(JobContext jc) { + DataManager manager = mActivity.getDataManager(); + ArrayList<Uri> uris = new ArrayList<Uri>(items.size()); + int index = 0; + for (Path path : items) { + if ((manager.getSupportedOperations(path) + & MediaObject.SUPPORT_SHARE) != 0) { + uris.add(manager.getContentUri(path)); + } + onProgressUpdate(++index, null); + } + if (jc.isCancelled()) return null; + Intent intent = new Intent() + .setComponent(component).setType(mimeType); + if (uris.isEmpty()) { + return null; + } else if (uris.size() == 1) { + intent.setAction(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_STREAM, uris.get(0)); + } else { + intent.setAction(Intent.ACTION_SEND_MULTIPLE); + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); + } + onProgressComplete(EXECUTION_RESULT_SUCCESS, null); + mHandler.sendMessage(mHandler.obtainMessage(MSG_DO_SHARE, intent)); + return null; + } + }, null); + } + + private static void setMenuItemVisibility( + Menu menu, int id, boolean visibility) { + MenuItem item = menu.findItem(id); + if (item != null) item.setVisible(visibility); + } + + public static void updateMenuOperation(Menu menu, int supported) { + boolean supportDelete = (supported & MediaObject.SUPPORT_DELETE) != 0; + boolean supportRotate = (supported & MediaObject.SUPPORT_ROTATE) != 0; + boolean supportCrop = (supported & MediaObject.SUPPORT_CROP) != 0; + boolean supportShare = (supported & MediaObject.SUPPORT_SHARE) != 0; + boolean supportSetAs = (supported & MediaObject.SUPPORT_SETAS) != 0; + boolean supportShowOnMap = (supported & MediaObject.SUPPORT_SHOW_ON_MAP) != 0; + boolean supportCache = (supported & MediaObject.SUPPORT_CACHE) != 0; + boolean supportEdit = (supported & MediaObject.SUPPORT_EDIT) != 0; + boolean supportInfo = (supported & MediaObject.SUPPORT_INFO) != 0; + boolean supportImport = (supported & MediaObject.SUPPORT_IMPORT) != 0; + + setMenuItemVisibility(menu, R.id.action_delete, supportDelete); + setMenuItemVisibility(menu, R.id.action_rotate_ccw, supportRotate); + setMenuItemVisibility(menu, R.id.action_rotate_cw, supportRotate); + setMenuItemVisibility(menu, R.id.action_crop, supportCrop); + setMenuItemVisibility(menu, R.id.action_share, supportShare); + setMenuItemVisibility(menu, R.id.action_setas, supportSetAs); + setMenuItemVisibility(menu, R.id.action_show_on_map, supportShowOnMap); + setMenuItemVisibility(menu, R.id.action_edit, supportEdit); + setMenuItemVisibility(menu, R.id.action_details, supportInfo); + setMenuItemVisibility(menu, R.id.action_import, supportImport); + } + + private Path getSingleSelectedPath() { + ArrayList<Path> ids = mSelectionManager.getSelected(true); + Utils.assertTrue(ids.size() == 1); + return ids.get(0); + } + + public boolean onMenuClicked(MenuItem menuItem, ProgressListener listener) { + int title; + DataManager manager = mActivity.getDataManager(); + int action = menuItem.getItemId(); + switch (action) { + case R.id.action_select_all: + if (mSelectionManager.inSelectAllMode()) { + mSelectionManager.deSelectAll(); + } else { + mSelectionManager.selectAll(); + } + return true; + case R.id.action_crop: { + Path path = getSingleSelectedPath(); + String mimeType = getMimeType(manager.getMediaType(path)); + Intent intent = new Intent(CropImage.ACTION_CROP) + .setDataAndType(manager.getContentUri(path), mimeType); + ((Activity) mActivity).startActivity(intent); + return true; + } + case R.id.action_setas: { + Path path = getSingleSelectedPath(); + int type = manager.getMediaType(path); + Intent intent = new Intent(Intent.ACTION_ATTACH_DATA); + String mimeType = getMimeType(type); + intent.setDataAndType(manager.getContentUri(path), mimeType); + intent.putExtra("mimeType", mimeType); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + Activity activity = (Activity) mActivity; + activity.startActivity(Intent.createChooser( + intent, activity.getString(R.string.set_as))); + return true; + } + case R.id.action_confirm_delete: + title = R.string.delete; + break; + case R.id.action_rotate_cw: + title = R.string.rotate_right; + break; + case R.id.action_rotate_ccw: + title = R.string.rotate_left; + break; + case R.id.action_show_on_map: + title = R.string.show_on_map; + break; + case R.id.action_edit: + title = R.string.edit; + break; + case R.id.action_import: + title = R.string.Import; + break; + default: + return false; + } + startAction(action, title, listener); + return true; + } + + public void startAction(int action, int title, ProgressListener listener) { + ArrayList<Path> ids = mSelectionManager.getSelected(false); + Utils.assertTrue(mDialog == null); + + Activity activity = (Activity) mActivity; + mDialog = showProgressDialog(activity, title, ids.size()); + MediaOperation operation = new MediaOperation(action, ids, listener); + mTask = mActivity.getThreadPool().submit(operation, null); + } + + public static String getMimeType(int type) { + switch (type) { + case MediaObject.MEDIA_TYPE_IMAGE : + return "image/*"; + case MediaObject.MEDIA_TYPE_VIDEO : + return "video/*"; + default: return "*/*"; + } + } + + private boolean execute( + DataManager manager, JobContext jc, int cmd, Path path) { + boolean result = true; + switch (cmd) { + case R.id.action_confirm_delete: + manager.delete(path); + break; + case R.id.action_rotate_cw: + manager.rotate(path, 90); + break; + case R.id.action_rotate_ccw: + manager.rotate(path, -90); + break; + case R.id.action_toggle_full_caching: { + MediaObject obj = manager.getMediaObject(path); + int cacheFlag = obj.getCacheFlag(); + if (cacheFlag == MediaObject.CACHE_FLAG_FULL) { + cacheFlag = MediaObject.CACHE_FLAG_SCREENNAIL; + } else { + cacheFlag = MediaObject.CACHE_FLAG_FULL; + } + obj.cache(cacheFlag); + break; + } + case R.id.action_show_on_map: { + MediaItem item = (MediaItem) manager.getMediaObject(path); + double latlng[] = new double[2]; + item.getLatLong(latlng); + if (GalleryUtils.isValidLocation(latlng[0], latlng[1])) { + GalleryUtils.showOnMap((Context) mActivity, latlng[0], latlng[1]); + } + break; + } + case R.id.action_import: { + MediaObject obj = manager.getMediaObject(path); + result = obj.Import(); + break; + } + case R.id.action_edit: { + Activity activity = (Activity) mActivity; + MediaItem item = (MediaItem) manager.getMediaObject(path); + try { + activity.startActivity(Intent.createChooser( + new Intent(Intent.ACTION_EDIT) + .setData(item.getContentUri()) + .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION), + null)); + } catch (Throwable t) { + Log.w(TAG, "failed to start edit activity: ", t); + Toast.makeText(activity, + activity.getString(R.string.activity_not_found), + Toast.LENGTH_SHORT).show(); + } + break; + } + default: + throw new AssertionError(); + } + return result; + } + + private class MediaOperation implements Job<Void> { + private final ArrayList<Path> mItems; + private final int mOperation; + private final ProgressListener mListener; + + public MediaOperation(int operation, ArrayList<Path> items, ProgressListener listener) { + mOperation = operation; + mItems = items; + mListener = listener; + } + + public Void run(JobContext jc) { + int index = 0; + DataManager manager = mActivity.getDataManager(); + int result = EXECUTION_RESULT_SUCCESS; + for (Path id : mItems) { + if (jc.isCancelled()) { + result = EXECUTION_RESULT_CANCEL; + break; + } + try { + if (!execute(manager, jc, mOperation, id)) result = EXECUTION_RESULT_FAIL; + } catch (Throwable th) { + Log.e(TAG, "failed to execute operation " + mOperation + + " for " + id, th); + } + onProgressUpdate(index++, mListener); + } + onProgressComplete(result, mListener); + return null; + } + } +} + diff --git a/src/com/android/gallery3d/ui/MultiLineTexture.java b/src/com/android/gallery3d/ui/MultiLineTexture.java new file mode 100644 index 000000000..be62d59c0 --- /dev/null +++ b/src/com/android/gallery3d/ui/MultiLineTexture.java @@ -0,0 +1,50 @@ +/* + * 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.ui; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; + +// MultiLineTexture is a texture shows the content of a specified String. +// +// To create a MultiLineTexture, use the newInstance() method and specify +// the String, the font size, and the color. +class MultiLineTexture extends CanvasTexture { + private final Layout mLayout; + + private MultiLineTexture(Layout layout) { + super(layout.getWidth(), layout.getHeight()); + mLayout = layout; + } + + public static MultiLineTexture newInstance( + String text, int maxWidth, float textSize, int color) { + TextPaint paint = StringTexture.getDefaultPaint(textSize, color); + Layout layout = new StaticLayout(text, 0, text.length(), paint, + maxWidth, Layout.Alignment.ALIGN_NORMAL, 1, 0, true, null, 0); + + return new MultiLineTexture(layout); + } + + @Override + protected void onDraw(Canvas canvas, Bitmap backing) { + mLayout.draw(canvas); + } +} diff --git a/src/com/android/gallery3d/ui/NinePatchChunk.java b/src/com/android/gallery3d/ui/NinePatchChunk.java new file mode 100644 index 000000000..61bf22c33 --- /dev/null +++ b/src/com/android/gallery3d/ui/NinePatchChunk.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.ui; + +import android.graphics.Rect; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +// See "frameworks/base/include/utils/ResourceTypes.h" for the format of +// NinePatch chunk. +class NinePatchChunk { + + public static final int NO_COLOR = 0x00000001; + public static final int TRANSPARENT_COLOR = 0x00000000; + + public Rect mPaddings = new Rect(); + + public int mDivX[]; + public int mDivY[]; + public int mColor[]; + + private static void readIntArray(int[] data, ByteBuffer buffer) { + for (int i = 0, n = data.length; i < n; ++i) { + data[i] = buffer.getInt(); + } + } + + private static void checkDivCount(int length) { + if (length == 0 || (length & 0x01) != 0) { + throw new RuntimeException("invalid nine-patch: " + length); + } + } + + public static NinePatchChunk deserialize(byte[] data) { + ByteBuffer byteBuffer = + ByteBuffer.wrap(data).order(ByteOrder.nativeOrder()); + + byte wasSerialized = byteBuffer.get(); + if (wasSerialized == 0) return null; + + NinePatchChunk chunk = new NinePatchChunk(); + chunk.mDivX = new int[byteBuffer.get()]; + chunk.mDivY = new int[byteBuffer.get()]; + chunk.mColor = new int[byteBuffer.get()]; + + checkDivCount(chunk.mDivX.length); + checkDivCount(chunk.mDivY.length); + + // skip 8 bytes + byteBuffer.getInt(); + byteBuffer.getInt(); + + chunk.mPaddings.left = byteBuffer.getInt(); + chunk.mPaddings.right = byteBuffer.getInt(); + chunk.mPaddings.top = byteBuffer.getInt(); + chunk.mPaddings.bottom = byteBuffer.getInt(); + + // skip 4 bytes + byteBuffer.getInt(); + + readIntArray(chunk.mDivX, byteBuffer); + readIntArray(chunk.mDivY, byteBuffer); + readIntArray(chunk.mColor, byteBuffer); + + return chunk; + } +}
\ No newline at end of file diff --git a/src/com/android/gallery3d/ui/NinePatchTexture.java b/src/com/android/gallery3d/ui/NinePatchTexture.java new file mode 100644 index 000000000..15b057a92 --- /dev/null +++ b/src/com/android/gallery3d/ui/NinePatchTexture.java @@ -0,0 +1,401 @@ +/* + * 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.ui; + +import com.android.gallery3d.common.Utils; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Rect; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.microedition.khronos.opengles.GL11; + +// NinePatchTexture is a texture backed by a NinePatch resource. +// +// getPaddings() returns paddings specified in the NinePatch. +// getNinePatchChunk() returns the layout data specified in the NinePatch. +// +public class NinePatchTexture extends ResourceTexture { + @SuppressWarnings("unused") + private static final String TAG = "NinePatchTexture"; + private NinePatchChunk mChunk; + private MyCacheMap<Long, NinePatchInstance> mInstanceCache = + new MyCacheMap<Long, NinePatchInstance>(); + + public NinePatchTexture(Context context, int resId) { + super(context, resId); + } + + @Override + protected Bitmap onGetBitmap() { + if (mBitmap != null) return mBitmap; + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + Bitmap bitmap = BitmapFactory.decodeResource( + mContext.getResources(), mResId, options); + mBitmap = bitmap; + setSize(bitmap.getWidth(), bitmap.getHeight()); + byte[] chunkData = bitmap.getNinePatchChunk(); + mChunk = chunkData == null + ? null + : NinePatchChunk.deserialize(bitmap.getNinePatchChunk()); + if (mChunk == null) { + throw new RuntimeException("invalid nine-patch image: " + mResId); + } + return bitmap; + } + + public Rect getPaddings() { + // get the paddings from nine patch + if (mChunk == null) onGetBitmap(); + return mChunk.mPaddings; + } + + public NinePatchChunk getNinePatchChunk() { + if (mChunk == null) onGetBitmap(); + return mChunk; + } + + private static class MyCacheMap<K, V> extends LinkedHashMap<K, V> { + private int CACHE_SIZE = 16; + private V mJustRemoved; + + public MyCacheMap() { + super(4, 0.75f, true); + } + + @Override + protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { + if (size() > CACHE_SIZE) { + mJustRemoved = eldest.getValue(); + return true; + } + return false; + } + + public V getJustRemoved() { + V result = mJustRemoved; + mJustRemoved = null; + return result; + } + } + + private NinePatchInstance findInstance(GLCanvas canvas, int w, int h) { + long key = w; + key = (key << 32) | h; + NinePatchInstance instance = mInstanceCache.get(key); + + if (instance == null) { + instance = new NinePatchInstance(this, w, h); + mInstanceCache.put(key, instance); + NinePatchInstance removed = mInstanceCache.getJustRemoved(); + if (removed != null) { + removed.recycle(canvas); + } + } + + return instance; + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int w, int h) { + if (!isLoaded(canvas)) { + mInstanceCache.clear(); + } + + if (w != 0 && h != 0) { + findInstance(canvas, w, h).draw(canvas, this, x, y); + } + } + + @Override + public void recycle() { + super.recycle(); + GLCanvas canvas = mCanvasRef == null ? null : mCanvasRef.get(); + if (canvas == null) return; + for (NinePatchInstance instance : mInstanceCache.values()) { + instance.recycle(canvas); + } + mInstanceCache.clear(); + } +} + +// This keeps data for a specialization of NinePatchTexture with the size +// (width, height). We pre-compute the coordinates for efficiency. +class NinePatchInstance { + + @SuppressWarnings("unused") + private static final String TAG = "NinePatchInstance"; + + // We need 16 vertices for a normal nine-patch image (the 4x4 vertices) + private static final int VERTEX_BUFFER_SIZE = 16 * 2; + + // We need 22 indices for a normal nine-patch image, plus 2 for each + // transparent region. Current there are at most 1 transparent region. + private static final int INDEX_BUFFER_SIZE = 22 + 2; + + private FloatBuffer mXyBuffer; + private FloatBuffer mUvBuffer; + private ByteBuffer mIndexBuffer; + + // Names for buffer names: xy, uv, index. + private int[] mBufferNames; + + private int mIdxCount; + + public NinePatchInstance(NinePatchTexture tex, int width, int height) { + NinePatchChunk chunk = tex.getNinePatchChunk(); + + if (width <= 0 || height <= 0) { + throw new RuntimeException("invalid dimension"); + } + + // The code should be easily extended to handle the general cases by + // allocating more space for buffers. But let's just handle the only + // use case. + if (chunk.mDivX.length != 2 || chunk.mDivY.length != 2) { + throw new RuntimeException("unsupported nine patch"); + } + + float divX[] = new float[4]; + float divY[] = new float[4]; + float divU[] = new float[4]; + float divV[] = new float[4]; + + int nx = stretch(divX, divU, chunk.mDivX, tex.getWidth(), width); + int ny = stretch(divY, divV, chunk.mDivY, tex.getHeight(), height); + + prepareVertexData(divX, divY, divU, divV, nx, ny, chunk.mColor); + } + + /** + * Stretches the texture according to the nine-patch rules. It will + * linearly distribute the strechy parts defined in the nine-patch chunk to + * the target area. + * + * <pre> + * source + * /--------------^---------------\ + * u0 u1 u2 u3 u4 u5 + * div ---> |fffff|ssssssss|fff|ssssss|ffff| ---> u + * | div0 div1 div2 div3 | + * | | / / / / + * | | / / / / + * | | / / / / + * |fffff|ssss|fff|sss|ffff| ---> x + * x0 x1 x2 x3 x4 x5 + * \----------v------------/ + * target + * + * f: fixed segment + * s: stretchy segment + * </pre> + * + * @param div the stretch parts defined in nine-patch chunk + * @param source the length of the texture + * @param target the length on the drawing plan + * @param u output, the positions of these dividers in the texture + * coordinate + * @param x output, the corresponding position of these dividers on the + * drawing plan + * @return the number of these dividers. + */ + private static int stretch( + float x[], float u[], int div[], int source, int target) { + int textureSize = Utils.nextPowerOf2(source); + float textureBound = (float) source / textureSize; + + float stretch = 0; + for (int i = 0, n = div.length; i < n; i += 2) { + stretch += div[i + 1] - div[i]; + } + + float remaining = target - source + stretch; + + float lastX = 0; + float lastU = 0; + + x[0] = 0; + u[0] = 0; + for (int i = 0, n = div.length; i < n; i += 2) { + // Make the stretchy segment a little smaller to prevent sampling + // on neighboring fixed segments. + // fixed segment + x[i + 1] = lastX + (div[i] - lastU) + 0.5f; + u[i + 1] = Math.min((div[i] + 0.5f) / textureSize, textureBound); + + // stretchy segment + float partU = div[i + 1] - div[i]; + float partX = remaining * partU / stretch; + remaining -= partX; + stretch -= partU; + + lastX = x[i + 1] + partX; + lastU = div[i + 1]; + x[i + 2] = lastX - 0.5f; + u[i + 2] = Math.min((lastU - 0.5f)/ textureSize, textureBound); + } + // the last fixed segment + x[div.length + 1] = target; + u[div.length + 1] = textureBound; + + // remove segments with length 0. + int last = 0; + for (int i = 1, n = div.length + 2; i < n; ++i) { + if ((x[i] - x[last]) < 1f) continue; + x[++last] = x[i]; + u[last] = u[i]; + } + return last + 1; + } + + private void prepareVertexData(float x[], float y[], float u[], float v[], + int nx, int ny, int[] color) { + /* + * Given a 3x3 nine-patch image, the vertex order is defined as the + * following graph: + * + * (0) (1) (2) (3) + * | /| /| /| + * | / | / | / | + * (4) (5) (6) (7) + * | \ | \ | \ | + * | \| \| \| + * (8) (9) (A) (B) + * | /| /| /| + * | / | / | / | + * (C) (D) (E) (F) + * + * And we draw the triangle strip in the following index order: + * + * index: 04152637B6A5948C9DAEBF + */ + int pntCount = 0; + float xy[] = new float[VERTEX_BUFFER_SIZE]; + float uv[] = new float[VERTEX_BUFFER_SIZE]; + for (int j = 0; j < ny; ++j) { + for (int i = 0; i < nx; ++i) { + int xIndex = (pntCount++) << 1; + int yIndex = xIndex + 1; + xy[xIndex] = x[i]; + xy[yIndex] = y[j]; + uv[xIndex] = u[i]; + uv[yIndex] = v[j]; + } + } + + int idxCount = 1; + boolean isForward = false; + byte index[] = new byte[INDEX_BUFFER_SIZE]; + for (int row = 0; row < ny - 1; row++) { + --idxCount; + isForward = !isForward; + + int start, end, inc; + if (isForward) { + start = 0; + end = nx; + inc = 1; + } else { + start = nx - 1; + end = -1; + inc = -1; + } + + for (int col = start; col != end; col += inc) { + int k = row * nx + col; + if (col != start) { + int colorIdx = row * (nx - 1) + col; + if (isForward) colorIdx--; + if (color[colorIdx] == NinePatchChunk.TRANSPARENT_COLOR) { + index[idxCount] = index[idxCount - 1]; + ++idxCount; + index[idxCount++] = (byte) k; + } + } + + index[idxCount++] = (byte) k; + index[idxCount++] = (byte) (k + nx); + } + } + + mIdxCount = idxCount; + + int size = (pntCount * 2) * (Float.SIZE / Byte.SIZE); + mXyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer(); + mUvBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer(); + mIndexBuffer = allocateDirectNativeOrderBuffer(mIdxCount); + + mXyBuffer.put(xy, 0, pntCount * 2).position(0); + mUvBuffer.put(uv, 0, pntCount * 2).position(0); + mIndexBuffer.put(index, 0, idxCount).position(0); + } + + private static ByteBuffer allocateDirectNativeOrderBuffer(int size) { + return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()); + } + + private void prepareBuffers(GLCanvas canvas) { + mBufferNames = new int[3]; + GL11 gl = canvas.getGLInstance(); + gl.glGenBuffers(3, mBufferNames, 0); + + gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBufferNames[0]); + gl.glBufferData(GL11.GL_ARRAY_BUFFER, + mXyBuffer.capacity() * (Float.SIZE / Byte.SIZE), + mXyBuffer, GL11.GL_STATIC_DRAW); + + gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBufferNames[1]); + gl.glBufferData(GL11.GL_ARRAY_BUFFER, + mUvBuffer.capacity() * (Float.SIZE / Byte.SIZE), + mUvBuffer, GL11.GL_STATIC_DRAW); + + gl.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, mBufferNames[2]); + gl.glBufferData(GL11.GL_ELEMENT_ARRAY_BUFFER, + mIndexBuffer.capacity(), + mIndexBuffer, GL11.GL_STATIC_DRAW); + + // These buffers are never used again. + mXyBuffer = null; + mUvBuffer = null; + mIndexBuffer = null; + } + + public void draw(GLCanvas canvas, NinePatchTexture tex, int x, int y) { + if (mBufferNames == null) { + prepareBuffers(canvas); + } + canvas.drawMesh(tex, x, y, mBufferNames[0], mBufferNames[1], + mBufferNames[2], mIdxCount); + } + + public void recycle(GLCanvas canvas) { + if (mBufferNames != null) { + canvas.deleteBuffer(mBufferNames[0]); + canvas.deleteBuffer(mBufferNames[1]); + canvas.deleteBuffer(mBufferNames[2]); + mBufferNames = null; + } + } +} diff --git a/src/com/android/gallery3d/ui/OnSelectedListener.java b/src/com/android/gallery3d/ui/OnSelectedListener.java new file mode 100644 index 000000000..2cc5809bf --- /dev/null +++ b/src/com/android/gallery3d/ui/OnSelectedListener.java @@ -0,0 +1,21 @@ +/* + * 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.ui; + +public interface OnSelectedListener { + public void onSelected(GLView source); +} diff --git a/src/com/android/gallery3d/ui/Paper.java b/src/com/android/gallery3d/ui/Paper.java new file mode 100644 index 000000000..641fc2c8e --- /dev/null +++ b/src/com/android/gallery3d/ui/Paper.java @@ -0,0 +1,112 @@ +/* + * 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.ui; + +import com.android.gallery3d.ui.PositionRepository.Position; +import com.android.gallery3d.util.GalleryUtils; + +import android.opengl.Matrix; + +import javax.microedition.khronos.opengles.GL11; +import javax.microedition.khronos.opengles.GL11ExtensionPack; + +// This class does the overscroll effect. +class Paper { + private static final String TAG = "Paper"; + private static final int ROTATE_FACTOR = 4; + private OverscrollAnimation mAnimationLeft = new OverscrollAnimation(); + private OverscrollAnimation mAnimationRight = new OverscrollAnimation(); + private int mWidth, mHeight; + private float[] mMatrix = new float[16]; + + public void overScroll(float distance) { + if (distance < 0) { + mAnimationLeft.scroll(-distance); + } else { + mAnimationRight.scroll(distance); + } + } + + public boolean advanceAnimation(long currentTimeMillis) { + return mAnimationLeft.advanceAnimation(currentTimeMillis) + | mAnimationRight.advanceAnimation(currentTimeMillis); + } + + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + } + + public float[] getTransform(Position target, Position base, + float scrollX, float scrollY) { + float left = mAnimationLeft.getValue(); + float right = mAnimationRight.getValue(); + float screenX = target.x - scrollX; + float t = ((mWidth - screenX) * left - screenX * right) / (mWidth * mWidth); + // compress t to the range (-1, 1) by the function + // f(t) = (1 / (1 + e^-t) - 0.5) * 2 + // then multiply by 90 to make the range (-45, 45) + float degrees = + (1 / (1 + (float) Math.exp(-t * ROTATE_FACTOR)) - 0.5f) * 2 * -45; + Matrix.setIdentityM(mMatrix, 0); + Matrix.translateM(mMatrix, 0, mMatrix, 0, base.x, base.y, base.z); + Matrix.rotateM(mMatrix, 0, degrees, 0, 1, 0); + Matrix.translateM(mMatrix, 0, mMatrix, 0, + target.x - base.x, target.y - base.y, target.z - base.z); + return mMatrix; + } +} + +class OverscrollAnimation { + private static final String TAG = "OverscrollAnimation"; + private static final long START_ANIMATION = -1; + private static final long NO_ANIMATION = -2; + private static final long ANIMATION_DURATION = 500; + + private long mAnimationStartTime = NO_ANIMATION; + private float mVelocity; + private float mCurrentValue; + + public void scroll(float distance) { + mAnimationStartTime = START_ANIMATION; + mCurrentValue += distance; + } + + public boolean advanceAnimation(long currentTimeMillis) { + if (mAnimationStartTime == NO_ANIMATION) return false; + if (mAnimationStartTime == START_ANIMATION) { + mAnimationStartTime = currentTimeMillis; + return true; + } + + long deltaTime = currentTimeMillis - mAnimationStartTime; + float t = deltaTime / 100f; + mCurrentValue *= Math.pow(0.5f, t); + mAnimationStartTime = currentTimeMillis; + + if (mCurrentValue < 1) { + mAnimationStartTime = NO_ANIMATION; + mCurrentValue = 0; + return false; + } + return true; + } + + public float getValue() { + return mCurrentValue; + } +} diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java new file mode 100644 index 000000000..aba572b00 --- /dev/null +++ b/src/com/android/gallery3d/ui/PhotoView.java @@ -0,0 +1,1191 @@ +/* + * 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.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.PositionRepository.Position; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.RectF; +import android.os.Message; +import android.os.SystemClock; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; + +public class PhotoView extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "PhotoView"; + + public static final int INVALID_SIZE = -1; + + private static final int MSG_TRANSITION_COMPLETE = 1; + private static final int MSG_SHOW_LOADING = 2; + + private static final long DELAY_SHOW_LOADING = 250; // 250ms; + + private static final int TRANS_NONE = 0; + private static final int TRANS_SWITCH_NEXT = 3; + private static final int TRANS_SWITCH_PREVIOUS = 4; + + public static final int TRANS_SLIDE_IN_RIGHT = 1; + public static final int TRANS_SLIDE_IN_LEFT = 2; + public static final int TRANS_OPEN_ANIMATION = 5; + + private static final int LOADING_INIT = 0; + private static final int LOADING_TIMEOUT = 1; + private static final int LOADING_COMPLETE = 2; + private static final int LOADING_FAIL = 3; + + private static final int ENTRY_PREVIOUS = 0; + private static final int ENTRY_NEXT = 1; + + private static final int IMAGE_GAP = 96; + private static final int SWITCH_THRESHOLD = 256; + private static final float SWIPE_THRESHOLD = 300f; + + private static final float DEFAULT_TEXT_SIZE = 20; + + // We try to scale up the image to fill the screen. But in order not to + // scale too much for small icons, we limit the max up-scaling factor here. + private static final float SCALE_LIMIT = 4; + + public interface PhotoTapListener { + public void onSingleTapUp(int x, int y); + } + + // the previous/next image entries + private final ScreenNailEntry mScreenNails[] = new ScreenNailEntry[2]; + + private final ScaleGestureDetector mScaleDetector; + private final GestureDetector mGestureDetector; + private final DownUpDetector mDownUpDetector; + + private PhotoTapListener mPhotoTapListener; + + private final PositionController mPositionController; + + private Model mModel; + private StringTexture mLoadingText; + private StringTexture mNoThumbnailText; + private int mTransitionMode = TRANS_NONE; + private final TileImageView mTileView; + private Texture mVideoPlayIcon; + + private boolean mShowVideoPlayIcon; + private ProgressSpinner mLoadingSpinner; + + private SynchronizedHandler mHandler; + + private int mLoadingState = LOADING_COMPLETE; + + private RectF mTempRect = new RectF(); + private float[] mTempPoints = new float[8]; + + private int mImageRotation; + + private Path mOpenedItemPath; + private GalleryActivity mActivity; + + public PhotoView(GalleryActivity activity) { + mActivity = activity; + mTileView = new TileImageView(activity); + addComponent(mTileView); + Context context = activity.getAndroidContext(); + mLoadingSpinner = new ProgressSpinner(context); + mLoadingText = StringTexture.newInstance( + context.getString(R.string.loading), + DEFAULT_TEXT_SIZE, Color.WHITE); + mNoThumbnailText = StringTexture.newInstance( + context.getString(R.string.no_thumbnail), + DEFAULT_TEXT_SIZE, Color.WHITE); + + mHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_TRANSITION_COMPLETE: { + onTransitionComplete(); + break; + } + case MSG_SHOW_LOADING: { + if (mLoadingState == LOADING_INIT) { + // We don't need the opening animation + mOpenedItemPath = null; + + mLoadingSpinner.startAnimation(); + mLoadingState = LOADING_TIMEOUT; + invalidate(); + } + break; + } + default: throw new AssertionError(message.what); + } + } + }; + + mGestureDetector = new GestureDetector(context, + new MyGestureListener(), null, true /* ignoreMultitouch */); + mScaleDetector = new ScaleGestureDetector(context, new MyScaleListener()); + mDownUpDetector = new DownUpDetector(new MyDownUpListener()); + + for (int i = 0, n = mScreenNails.length; i < n; ++i) { + mScreenNails[i] = new ScreenNailEntry(); + } + + mPositionController = new PositionController(this); + mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play); + } + + + public void setModel(Model model) { + if (mModel == model) return; + mModel = model; + mTileView.setModel(model); + if (model != null) notifyOnNewImage(); + } + + public void setPhotoTapListener(PhotoTapListener listener) { + mPhotoTapListener = listener; + } + + private boolean setTileViewPosition(int centerX, int centerY, float scale) { + int inverseX = mPositionController.mImageW - centerX; + int inverseY = mPositionController.mImageH - centerY; + TileImageView t = mTileView; + int rotation = mImageRotation; + switch (rotation) { + case 0: return t.setPosition(centerX, centerY, scale, 0); + case 90: return t.setPosition(centerY, inverseX, scale, 90); + case 180: return t.setPosition(inverseX, inverseY, scale, 180); + case 270: return t.setPosition(inverseY, centerX, scale, 270); + default: throw new IllegalArgumentException(String.valueOf(rotation)); + } + } + + public void setPosition(int centerX, int centerY, float scale) { + if (setTileViewPosition(centerX, centerY, scale)) { + layoutScreenNails(); + } + } + + private void updateScreenNailEntry(int which, ImageData data) { + if (mTransitionMode == TRANS_SWITCH_NEXT + || mTransitionMode == TRANS_SWITCH_PREVIOUS) { + // ignore screen nail updating during switching + return; + } + ScreenNailEntry entry = mScreenNails[which]; + if (data == null) { + entry.set(false, null, 0); + } else { + entry.set(true, data.bitmap, data.rotation); + } + } + + // -1 previous, 0 current, 1 next + public void notifyImageInvalidated(int which) { + switch (which) { + case -1: { + updateScreenNailEntry( + ENTRY_PREVIOUS, mModel.getPreviousImage()); + layoutScreenNails(); + invalidate(); + break; + } + case 1: { + updateScreenNailEntry(ENTRY_NEXT, mModel.getNextImage()); + layoutScreenNails(); + invalidate(); + break; + } + case 0: { + // mImageWidth and mImageHeight will get updated + mTileView.notifyModelInvalidated(); + + mImageRotation = mModel.getImageRotation(); + if (((mImageRotation / 90) & 1) == 0) { + mPositionController.setImageSize( + mTileView.mImageWidth, mTileView.mImageHeight); + } else { + mPositionController.setImageSize( + mTileView.mImageHeight, mTileView.mImageWidth); + } + updateLoadingState(); + break; + } + } + } + + private void updateLoadingState() { + // Possible transitions of mLoadingState: + // INIT --> TIMEOUT, COMPLETE, FAIL + // TIMEOUT --> COMPLETE, FAIL, INIT + // COMPLETE --> INIT + // FAIL --> INIT + if (mModel.getLevelCount() != 0 || mModel.getBackupImage() != null) { + mHandler.removeMessages(MSG_SHOW_LOADING); + mLoadingState = LOADING_COMPLETE; + } else if (mModel.isFailedToLoad()) { + mHandler.removeMessages(MSG_SHOW_LOADING); + mLoadingState = LOADING_FAIL; + } else if (mLoadingState != LOADING_INIT) { + mLoadingState = LOADING_INIT; + mHandler.removeMessages(MSG_SHOW_LOADING); + mHandler.sendEmptyMessageDelayed( + MSG_SHOW_LOADING, DELAY_SHOW_LOADING); + } + } + + public void notifyModelInvalidated() { + if (mModel == null) { + updateScreenNailEntry(ENTRY_PREVIOUS, null); + updateScreenNailEntry(ENTRY_NEXT, null); + } else { + updateScreenNailEntry(ENTRY_PREVIOUS, mModel.getPreviousImage()); + updateScreenNailEntry(ENTRY_NEXT, mModel.getNextImage()); + } + layoutScreenNails(); + + if (mModel == null) { + mTileView.notifyModelInvalidated(); + mImageRotation = 0; + mPositionController.setImageSize(0, 0); + updateLoadingState(); + } else { + notifyImageInvalidated(0); + } + } + + @Override + protected boolean onTouch(MotionEvent event) { + mGestureDetector.onTouchEvent(event); + mScaleDetector.onTouchEvent(event); + mDownUpDetector.onTouchEvent(event); + return true; + } + + @Override + protected void onLayout( + boolean changeSize, int left, int top, int right, int bottom) { + mTileView.layout(left, top, right, bottom); + if (changeSize) { + mPositionController.setViewSize(getWidth(), getHeight()); + for (ScreenNailEntry entry : mScreenNails) { + entry.updateDrawingSize(); + } + } + } + + private static int gapToSide(int imageWidth, int viewWidth) { + return Math.max(0, (viewWidth - imageWidth) / 2); + } + + private RectF getImageBounds() { + PositionController p = mPositionController; + float points[] = mTempPoints; + + /* + * (p0,p1)----------(p2,p3) + * | | + * | | + * (p4,p5)----------(p6,p7) + */ + points[0] = points[4] = -p.mCurrentX; + points[1] = points[3] = -p.mCurrentY; + points[2] = points[6] = p.mImageW - p.mCurrentX; + points[5] = points[7] = p.mImageH - p.mCurrentY; + + RectF rect = mTempRect; + rect.set(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, + Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY); + + float scale = p.mCurrentScale; + float offsetX = p.mViewW / 2; + float offsetY = p.mViewH / 2; + for (int i = 0; i < 4; ++i) { + float x = points[i + i] * scale + offsetX; + float y = points[i + i + 1] * scale + offsetY; + if (x < rect.left) rect.left = x; + if (x > rect.right) rect.right = x; + if (y < rect.top) rect.top = y; + if (y > rect.bottom) rect.bottom = y; + } + return rect; + } + + + /* + * Here is how we layout the screen nails + * + * previous current next + * ___________ ________________ __________ + * | _______ | | __________ | | ______ | + * | | | | | | right->| | | | | | + * | | |<-------->|<--left | | | | | | + * | |_______| | | | |__________| | | |______| | + * |___________| | |________________| |__________| + * | <--> gapToSide() + * | + * IMAGE_GAP + Max(previous.gapToSide(), current.gapToSide) + */ + private void layoutScreenNails() { + int width = getWidth(); + int height = getHeight(); + + // Use the image width in AC, since we may fake the size if the + // image is unavailable + RectF bounds = getImageBounds(); + int left = Math.round(bounds.left); + int right = Math.round(bounds.right); + int gap = gapToSide(right - left, width); + + // layout the previous image + ScreenNailEntry entry = mScreenNails[ENTRY_PREVIOUS]; + + if (entry.isEnabled()) { + entry.layoutRightEdgeAt(left - ( + IMAGE_GAP + Math.max(gap, entry.gapToSide()))); + } + + // layout the next image + entry = mScreenNails[ENTRY_NEXT]; + if (entry.isEnabled()) { + entry.layoutLeftEdgeAt(right + ( + IMAGE_GAP + Math.max(gap, entry.gapToSide()))); + } + } + + private static class PositionController { + private long mAnimationStartTime = NO_ANIMATION; + private static final long NO_ANIMATION = -1; + private static final long LAST_ANIMATION = -2; + + // Animation time in milliseconds. + private static final float ANIM_TIME_SCROLL = 0; + private static final float ANIM_TIME_SCALE = 50; + private static final float ANIM_TIME_SNAPBACK = 600; + private static final float ANIM_TIME_SLIDE = 400; + private static final float ANIM_TIME_ZOOM = 300; + + private int mAnimationKind; + private final static int ANIM_KIND_SCROLL = 0; + private final static int ANIM_KIND_SCALE = 1; + private final static int ANIM_KIND_SNAPBACK = 2; + private final static int ANIM_KIND_SLIDE = 3; + private final static int ANIM_KIND_ZOOM = 4; + + private PhotoView mViewer; + private int mImageW, mImageH; + private int mViewW, mViewH; + + // The X, Y are the coordinate on bitmap which shows on the center of + // the view. We always keep the mCurrent{X,Y,SCALE} sync with the actual + // values used currently. + private int mCurrentX, mFromX, mToX; + private int mCurrentY, mFromY, mToY; + private float mCurrentScale, mFromScale, mToScale; + + // The offsets from the center of the view to the user's focus point, + // converted to the bitmap domain. + private float mPrevOffsetX; + private float mPrevOffsetY; + private boolean mInScale; + private boolean mUseViewSize = true; + + // The limits for position and scale. + private float mScaleMin, mScaleMax = 4f; + + PositionController(PhotoView viewer) { + mViewer = viewer; + } + + public void setImageSize(int width, int height) { + + // If no image available, use view size. + if (width == 0 || height == 0) { + mUseViewSize = true; + mImageW = mViewW; + mImageH = mViewH; + mCurrentX = mImageW / 2; + mCurrentY = mImageH / 2; + mCurrentScale = 1; + mScaleMin = 1; + mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); + return; + } + + mUseViewSize = false; + + float ratio = Math.min( + (float) mImageW / width, (float) mImageH / height); + + mCurrentX = translate(mCurrentX, mImageW, width, ratio); + mCurrentY = translate(mCurrentY, mImageH, height, ratio); + mCurrentScale = mCurrentScale * ratio; + + mFromX = translate(mFromX, mImageW, width, ratio); + mFromY = translate(mFromY, mImageH, height, ratio); + mFromScale = mFromScale * ratio; + + mToX = translate(mToX, mImageW, width, ratio); + mToY = translate(mToY, mImageH, height, ratio); + mToScale = mToScale * ratio; + + mImageW = width; + mImageH = height; + + mScaleMin = getMinimalScale(width, height, 0); + + // Scale the new image to fit into the old one + if (mViewer.mOpenedItemPath != null) { + Position position = PositionRepository + .getInstance(mViewer.mActivity).get(Long.valueOf( + System.identityHashCode(mViewer.mOpenedItemPath))); + mViewer.mOpenedItemPath = null; + if (position != null) { + float scale = 240f / Math.min(width, height); + mCurrentX = Math.round((mViewW / 2f - position.x) / scale) + mImageW / 2; + mCurrentY = Math.round((mViewH / 2f - position.y) / scale) + mImageH / 2; + mCurrentScale = scale; + mViewer.mTransitionMode = TRANS_OPEN_ANIMATION; + startSnapback(); + } + } else if (mAnimationStartTime == NO_ANIMATION) { + mCurrentScale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax); + } + mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); + } + + public void zoomIn(float tapX, float tapY, float targetScale) { + if (targetScale > mScaleMax) targetScale = mScaleMax; + float scale = mCurrentScale; + float tempX = (tapX - mViewW / 2) / mCurrentScale + mCurrentX; + float tempY = (tapY - mViewH / 2) / mCurrentScale + mCurrentY; + + // mCurrentX + (mViewW / 2) * (1 / targetScale) < mImageW + // mCurrentX - (mViewW / 2) * (1 / targetScale) > 0 + float min = mViewW / 2.0f / targetScale; + float max = mImageW - mViewW / 2.0f / targetScale; + int targetX = (int) Utils.clamp(tempX, min, max); + + min = mViewH / 2.0f / targetScale; + max = mImageH - mViewH / 2.0f / targetScale; + int targetY = (int) Utils.clamp(tempY, min, max); + + // If the width of the image is less then the view, center the image + if (mImageW * targetScale < mViewW) targetX = mImageW / 2; + if (mImageH * targetScale < mViewH) targetY = mImageH / 2; + + startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM); + } + + public void resetToFullView() { + startAnimation(mImageW / 2, mImageH / 2, mScaleMin, ANIM_KIND_ZOOM); + } + + private float getMinimalScale(int w, int h, int rotation) { + return Math.min(SCALE_LIMIT, ((rotation / 90) & 0x01) == 0 + ? Math.min((float) mViewW / w, (float) mViewH / h) + : Math.min((float) mViewW / h, (float) mViewH / w)); + } + + private static int translate(int value, int size, int updateSize, float ratio) { + return Math.round( + (value + (updateSize * ratio - size) / 2f) / ratio); + } + + public void setViewSize(int viewW, int viewH) { + boolean needLayout = mViewW == 0 || mViewH == 0; + + mViewW = viewW; + mViewH = viewH; + + if (mUseViewSize) { + mImageW = viewW; + mImageH = viewH; + mCurrentX = mImageW / 2; + mCurrentY = mImageH / 2; + mCurrentScale = 1; + mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); + } else { + boolean wasMinScale = (mCurrentScale == mScaleMin); + mScaleMin = Math.min(SCALE_LIMIT, Math.min( + (float) viewW / mImageW, (float) viewH / mImageH)); + if (needLayout || mCurrentScale < mScaleMin || wasMinScale) { + mCurrentX = mImageW / 2; + mCurrentY = mImageH / 2; + mCurrentScale = mScaleMin; + mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); + } + } + } + + public void stopAnimation() { + mAnimationStartTime = NO_ANIMATION; + } + + public void skipAnimation() { + if (mAnimationStartTime == NO_ANIMATION) return; + mAnimationStartTime = NO_ANIMATION; + mCurrentX = mToX; + mCurrentY = mToY; + mCurrentScale = mToScale; + } + + public void scrollBy(float dx, float dy, int type) { + startAnimation(getTargetX() + Math.round(dx / mCurrentScale), + getTargetY() + Math.round(dy / mCurrentScale), + mCurrentScale, type); + } + + public void beginScale(float focusX, float focusY) { + mInScale = true; + mPrevOffsetX = (focusX - mViewW / 2f) / mCurrentScale; + mPrevOffsetY = (focusY - mViewH / 2f) / mCurrentScale; + } + + public void scaleBy(float s, float focusX, float focusY) { + + // The focus point should keep this position on the ImageView. + // So, mCurrentX + mPrevOffsetX = mCurrentX' + offsetX. + // mCurrentY + mPrevOffsetY = mCurrentY' + offsetY. + float offsetX = (focusX - mViewW / 2f) / mCurrentScale; + float offsetY = (focusY - mViewH / 2f) / mCurrentScale; + + startAnimation(getTargetX() - Math.round(offsetX - mPrevOffsetX), + getTargetY() - Math.round(offsetY - mPrevOffsetY), + getTargetScale() * s, ANIM_KIND_SCALE); + mPrevOffsetX = offsetX; + mPrevOffsetY = offsetY; + } + + public void endScale() { + mInScale = false; + startSnapbackIfNeeded(); + } + + public void up() { + startSnapback(); + } + + public void startSlideInAnimation(int fromX) { + mFromX = Math.round(fromX + (mImageW - mViewW) / 2f); + mFromY = Math.round(mImageH / 2f); + mCurrentX = mFromX; + mCurrentY = mFromY; + startAnimation(mImageW / 2, mImageH / 2, mCurrentScale, + ANIM_KIND_SLIDE); + } + + public void startHorizontalSlide(int distance) { + scrollBy(distance, 0, ANIM_KIND_SLIDE); + } + + private void startAnimation( + int centerX, int centerY, float scale, int kind) { + if (centerX == mCurrentX && centerY == mCurrentY + && scale == mCurrentScale) return; + + mFromX = mCurrentX; + mFromY = mCurrentY; + mFromScale = mCurrentScale; + + mToX = centerX; + mToY = centerY; + mToScale = Utils.clamp(scale, 0.6f * mScaleMin, 1.4f * mScaleMax); + + // If the scaled dimension is smaller than the view, + // force it to be in the center. + if (Math.floor(mImageH * mToScale) <= mViewH) { + mToY = mImageH / 2; + } + + mAnimationStartTime = SystemClock.uptimeMillis(); + mAnimationKind = kind; + if (advanceAnimation()) mViewer.invalidate(); + } + + // Returns true if redraw is needed. + public boolean advanceAnimation() { + if (mAnimationStartTime == NO_ANIMATION) { + return false; + } else if (mAnimationStartTime == LAST_ANIMATION) { + mAnimationStartTime = NO_ANIMATION; + if (mViewer.mTransitionMode != TRANS_NONE) { + mViewer.mHandler.sendEmptyMessage(MSG_TRANSITION_COMPLETE); + return false; + } else { + return startSnapbackIfNeeded(); + } + } + + float animationTime; + if (mAnimationKind == ANIM_KIND_SCROLL) { + animationTime = ANIM_TIME_SCROLL; + } else if (mAnimationKind == ANIM_KIND_SCALE) { + animationTime = ANIM_TIME_SCALE; + } else if (mAnimationKind == ANIM_KIND_SLIDE) { + animationTime = ANIM_TIME_SLIDE; + } else if (mAnimationKind == ANIM_KIND_ZOOM) { + animationTime = ANIM_TIME_ZOOM; + } else /* if (mAnimationKind == ANIM_KIND_SNAPBACK) */ { + animationTime = ANIM_TIME_SNAPBACK; + } + + float progress; + if (animationTime == 0) { + progress = 1; + } else { + long now = SystemClock.uptimeMillis(); + progress = (now - mAnimationStartTime) / animationTime; + } + + if (progress >= 1) { + progress = 1; + mCurrentX = mToX; + mCurrentY = mToY; + mCurrentScale = mToScale; + mAnimationStartTime = LAST_ANIMATION; + } else { + float f = 1 - progress; + if (mAnimationKind == ANIM_KIND_SCROLL) { + progress = 1 - f; // linear + } else if (mAnimationKind == ANIM_KIND_SCALE) { + progress = 1 - f * f; // quadratic + } else /* if mAnimationKind is ANIM_KIND_SNAPBACK, + ANIM_KIND_ZOOM or ANIM_KIND_SLIDE */ { + progress = 1 - f * f * f * f * f; // x^5 + } + linearInterpolate(progress); + } + mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); + return true; + } + + private void linearInterpolate(float progress) { + // To linearly interpolate the position, we have to translate the + // coordinates. The meaning of the translated point (x, y) is the + // coordinates of the center of the bitmap on the view component. + float fromX = mViewW / 2f + (mImageW / 2f - mFromX) * mFromScale; + float toX = mViewW / 2f + (mImageW / 2f - mToX) * mToScale; + float currentX = fromX + progress * (toX - fromX); + + float fromY = mViewH / 2f + (mImageH / 2f - mFromY) * mFromScale; + float toY = mViewH / 2f + (mImageH / 2f - mToY) * mToScale; + float currentY = fromY + progress * (toY - fromY); + + mCurrentScale = mFromScale + progress * (mToScale - mFromScale); + mCurrentX = Math.round( + mImageW / 2f + (mViewW / 2f - currentX) / mCurrentScale); + mCurrentY = Math.round( + mImageH / 2f + (mViewH / 2f - currentY) / mCurrentScale); + } + + // Returns true if redraw is needed. + private boolean startSnapbackIfNeeded() { + if (mAnimationStartTime != NO_ANIMATION) return false; + if (mInScale) return false; + if (mAnimationKind == ANIM_KIND_SCROLL && mViewer.isDown()) { + return false; + } + return startSnapback(); + } + + public boolean startSnapback() { + boolean needAnimation = false; + int x = mCurrentX; + int y = mCurrentY; + float scale = mCurrentScale; + + if (mCurrentScale < mScaleMin || mCurrentScale > mScaleMax) { + needAnimation = true; + scale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax); + } + + // The number of pixels when the edge is aligned. + int left = (int) Math.ceil(mViewW / (2 * scale)); + int right = mImageW - left; + int top = (int) Math.ceil(mViewH / (2 * scale)); + int bottom = mImageH - top; + + if (mImageW * scale > mViewW) { + if (mCurrentX < left) { + needAnimation = true; + x = left; + } else if (mCurrentX > right) { + needAnimation = true; + x = right; + } + } else if (mCurrentX != mImageW / 2) { + needAnimation = true; + x = mImageW / 2; + } + + if (mImageH * scale > mViewH) { + if (mCurrentY < top) { + needAnimation = true; + y = top; + } else if (mCurrentY > bottom) { + needAnimation = true; + y = bottom; + } + } else if (mCurrentY != mImageH / 2) { + needAnimation = true; + y = mImageH / 2; + } + + if (needAnimation) { + startAnimation(x, y, scale, ANIM_KIND_SNAPBACK); + } + + return needAnimation; + } + + private float getTargetScale() { + if (mAnimationStartTime == NO_ANIMATION + || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentScale; + return mToScale; + } + + private int getTargetX() { + if (mAnimationStartTime == NO_ANIMATION + || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentX; + return mToX; + } + + private int getTargetY() { + if (mAnimationStartTime == NO_ANIMATION + || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentY; + return mToY; + } + } + + @Override + protected void render(GLCanvas canvas) { + PositionController p = mPositionController; + + // Draw the current photo + if (mLoadingState == LOADING_COMPLETE) { + super.render(canvas); + } + + // Draw the previous and the next photo + if (mTransitionMode != TRANS_SLIDE_IN_LEFT + && mTransitionMode != TRANS_SLIDE_IN_RIGHT + && mTransitionMode != TRANS_OPEN_ANIMATION) { + ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS]; + ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT]; + + if (prevNail.mVisible) prevNail.draw(canvas); + if (nextNail.mVisible) nextNail.draw(canvas); + } + + // Draw the progress spinner and the text below it + // + // (x, y) is where we put the center of the spinner. + // s is the size of the video play icon, and we use s to layout text + // because we want to keep the text at the same place when the video + // play icon is shown instead of the spinner. + int w = getWidth(); + int h = getHeight(); + int x = Math.round(getImageBounds().centerX()); + int y = h / 2; + int s = Math.min(getWidth(), getHeight()) / 6; + + if (mLoadingState == LOADING_TIMEOUT) { + StringTexture m = mLoadingText; + ProgressSpinner r = mLoadingSpinner; + r.draw(canvas, x - r.getWidth() / 2, y - r.getHeight() / 2); + m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5); + invalidate(); // we need to keep the spinner rotating + } else if (mLoadingState == LOADING_FAIL) { + StringTexture m = mNoThumbnailText; + m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5); + } + + // Draw the video play icon (in the place where the spinner was) + if (mShowVideoPlayIcon + && mLoadingState != LOADING_INIT + && mLoadingState != LOADING_TIMEOUT) { + mVideoPlayIcon.draw(canvas, x - s / 2, y - s / 2, s, s); + } + + if (mPositionController.advanceAnimation()) invalidate(); + } + + private void stopCurrentSwipingIfNeeded() { + // Enable fast sweeping + if (mTransitionMode == TRANS_SWITCH_NEXT) { + mTransitionMode = TRANS_NONE; + mPositionController.stopAnimation(); + switchToNextImage(); + } else if (mTransitionMode == TRANS_SWITCH_PREVIOUS) { + mTransitionMode = TRANS_NONE; + mPositionController.stopAnimation(); + switchToPreviousImage(); + } + } + + private static boolean isAlmostEquals(float a, float b) { + float diff = a - b; + return (diff < 0 ? -diff : diff) < 0.02f; + } + + private boolean swipeImages(float velocity) { + if (mTransitionMode != TRANS_NONE + && mTransitionMode != TRANS_SWITCH_NEXT + && mTransitionMode != TRANS_SWITCH_PREVIOUS) return false; + + ScreenNailEntry next = mScreenNails[ENTRY_NEXT]; + ScreenNailEntry prev = mScreenNails[ENTRY_PREVIOUS]; + + int width = getWidth(); + + // If the edge of the current photo is visible and the sweeping velocity + // exceed the threshold, switch to next / previous image + PositionController controller = mPositionController; + if (isAlmostEquals(controller.mCurrentScale, controller.mScaleMin)) { + if (velocity < -SWIPE_THRESHOLD) { + stopCurrentSwipingIfNeeded(); + if (next.isEnabled()) { + mTransitionMode = TRANS_SWITCH_NEXT; + controller.startHorizontalSlide(next.mOffsetX - width / 2); + return true; + } + return false; + } + if (velocity > SWIPE_THRESHOLD) { + stopCurrentSwipingIfNeeded(); + if (prev.isEnabled()) { + mTransitionMode = TRANS_SWITCH_PREVIOUS; + controller.startHorizontalSlide(prev.mOffsetX - width / 2); + return true; + } + return false; + } + } + + if (mTransitionMode != TRANS_NONE) return false; + + // Decide whether to swiping to the next/prev image in the zoom-in case + RectF bounds = getImageBounds(); + int left = Math.round(bounds.left); + int right = Math.round(bounds.right); + int threshold = SWITCH_THRESHOLD + gapToSide(right - left, width); + + // If we have moved the picture a lot, switching. + if (next.isEnabled() && threshold < width - right) { + mTransitionMode = TRANS_SWITCH_NEXT; + controller.startHorizontalSlide(next.mOffsetX - width / 2); + return true; + } + if (prev.isEnabled() && threshold < left) { + mTransitionMode = TRANS_SWITCH_PREVIOUS; + controller.startHorizontalSlide(prev.mOffsetX - width / 2); + return true; + } + + return false; + } + + private boolean mIgnoreUpEvent = false; + + private class MyGestureListener + extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onScroll( + MotionEvent e1, MotionEvent e2, float dx, float dy) { + if (mTransitionMode != TRANS_NONE) return true; + mPositionController.scrollBy( + dx, dy, PositionController.ANIM_KIND_SCROLL); + return true; + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + if (mPhotoTapListener != null) { + mPhotoTapListener.onSingleTapUp((int) e.getX(), (int) e.getY()); + } + return true; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + mIgnoreUpEvent = true; + if (!swipeImages(velocityX) && mTransitionMode == TRANS_NONE) { + mPositionController.up(); + } + return true; + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + if (mTransitionMode != TRANS_NONE) return true; + PositionController controller = mPositionController; + float scale = controller.mCurrentScale; + // onDoubleTap happened on the second ACTION_DOWN. + // We need to ignore the next UP event. + mIgnoreUpEvent = true; + if (scale <= 1.0f || isAlmostEquals(scale, controller.mScaleMin)) { + controller.zoomIn( + e.getX(), e.getY(), Math.max(1.5f, scale * 1.5f)); + } else { + controller.resetToFullView(); + } + return true; + } + } + + private class MyScaleListener + extends ScaleGestureDetector.SimpleOnScaleGestureListener { + + @Override + public boolean onScale(ScaleGestureDetector detector) { + float scale = detector.getScaleFactor(); + if (Float.isNaN(scale) || Float.isInfinite(scale) + || mTransitionMode != TRANS_NONE) return true; + mPositionController.scaleBy(scale, + detector.getFocusX(), detector.getFocusY()); + return true; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + if (mTransitionMode != TRANS_NONE) return false; + mPositionController.beginScale( + detector.getFocusX(), detector.getFocusY()); + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mPositionController.endScale(); + swipeImages(0); + } + } + + public void notifyOnNewImage() { + mPositionController.setImageSize(0, 0); + } + + public void startSlideInAnimation(int direction) { + PositionController a = mPositionController; + a.stopAnimation(); + switch (direction) { + case TRANS_SLIDE_IN_LEFT: { + mTransitionMode = TRANS_SLIDE_IN_LEFT; + a.startSlideInAnimation(a.mViewW); + break; + } + case TRANS_SLIDE_IN_RIGHT: { + mTransitionMode = TRANS_SLIDE_IN_RIGHT; + a.startSlideInAnimation(-a.mViewW); + break; + } + default: throw new IllegalArgumentException(String.valueOf(direction)); + } + } + + private class MyDownUpListener implements DownUpDetector.DownUpListener { + public void onDown(MotionEvent e) { + } + + public void onUp(MotionEvent e) { + if (mIgnoreUpEvent) { + mIgnoreUpEvent = false; + return; + } + if (!swipeImages(0) && mTransitionMode == TRANS_NONE) { + mPositionController.up(); + } + } + } + + private void switchToNextImage() { + // We update the texture here directly to prevent texture uploading. + ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS]; + ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT]; + mTileView.invalidateTiles(); + if (prevNail.mTexture != null) prevNail.mTexture.recycle(); + prevNail.mTexture = mTileView.mBackupImage; + mTileView.mBackupImage = nextNail.mTexture; + nextNail.mTexture = null; + mModel.next(); + } + + private void switchToPreviousImage() { + // We update the texture here directly to prevent texture uploading. + ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS]; + ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT]; + mTileView.invalidateTiles(); + if (nextNail.mTexture != null) nextNail.mTexture.recycle(); + nextNail.mTexture = mTileView.mBackupImage; + mTileView.mBackupImage = prevNail.mTexture; + nextNail.mTexture = null; + mModel.previous(); + } + + private void onTransitionComplete() { + int mode = mTransitionMode; + mTransitionMode = TRANS_NONE; + + if (mModel == null) return; + if (mode == TRANS_SWITCH_NEXT) { + switchToNextImage(); + } else if (mode == TRANS_SWITCH_PREVIOUS) { + switchToPreviousImage(); + } + } + + private boolean isDown() { + return mDownUpDetector.isDown(); + } + + public static interface Model extends TileImageView.Model { + public void next(); + public void previous(); + public int getImageRotation(); + + // Return null if the specified image is unavailable. + public ImageData getNextImage(); + public ImageData getPreviousImage(); + } + + public static class ImageData { + public int rotation; + public Bitmap bitmap; + + public ImageData(Bitmap bitmap, int rotation) { + this.bitmap = bitmap; + this.rotation = rotation; + } + } + + private static int getRotated(int degree, int original, int theother) { + return ((degree / 90) & 1) == 0 ? original : theother; + } + + private class ScreenNailEntry { + private boolean mVisible; + private boolean mEnabled; + + private int mRotation; + private int mDrawWidth; + private int mDrawHeight; + private int mOffsetX; + + private BitmapTexture mTexture; + + public void set(boolean enabled, Bitmap bitmap, int rotation) { + mEnabled = enabled; + mRotation = rotation; + if (bitmap == null) { + if (mTexture != null) mTexture.recycle(); + mTexture = null; + } else { + if (mTexture != null) { + if (mTexture.getBitmap() != bitmap) { + mTexture.recycle(); + mTexture = new BitmapTexture(bitmap); + } + } else { + mTexture = new BitmapTexture(bitmap); + } + updateDrawingSize(); + } + } + + public void layoutRightEdgeAt(int x) { + mVisible = x > 0; + mOffsetX = x - getRotated( + mRotation, mDrawWidth, mDrawHeight) / 2; + } + + public void layoutLeftEdgeAt(int x) { + mVisible = x < getWidth(); + mOffsetX = x + getRotated( + mRotation, mDrawWidth, mDrawHeight) / 2; + } + + public int gapToSide() { + return ((mRotation / 90) & 1) != 0 + ? PhotoView.gapToSide(mDrawHeight, getWidth()) + : PhotoView.gapToSide(mDrawWidth, getWidth()); + } + + public void updateDrawingSize() { + if (mTexture == null) return; + + int width = mTexture.getWidth(); + int height = mTexture.getHeight(); + float s = mPositionController.getMinimalScale(width, height, mRotation); + mDrawWidth = Math.round(width * s); + mDrawHeight = Math.round(height * s); + } + + public boolean isEnabled() { + return mEnabled; + } + + public void draw(GLCanvas canvas) { + int x = mOffsetX; + int y = getHeight() / 2; + + if (mTexture != null) { + if (mRotation != 0) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + canvas.translate(x, y, 0); + canvas.rotate(mRotation, 0, 0, 1); //mRotation + canvas.translate(-x, -y, 0); + } + mTexture.draw(canvas, x - mDrawWidth / 2, y - mDrawHeight / 2, + mDrawWidth, mDrawHeight); + if (mRotation != 0) { + canvas.restore(); + } + } + } + } + + public void pause() { + mPositionController.skipAnimation(); + mTransitionMode = TRANS_NONE; + mTileView.freeTextures(); + for (ScreenNailEntry entry : mScreenNails) { + entry.set(false, null, 0); + } + } + + public void resume() { + mTileView.prepareTextures(); + } + + public void setOpenedItem(Path itemPath) { + mOpenedItemPath = itemPath; + } + + public void showVideoPlayIcon(boolean show) { + mShowVideoPlayIcon = show; + } +} diff --git a/src/com/android/gallery3d/ui/PositionProvider.java b/src/com/android/gallery3d/ui/PositionProvider.java new file mode 100644 index 000000000..930c61ee9 --- /dev/null +++ b/src/com/android/gallery3d/ui/PositionProvider.java @@ -0,0 +1,23 @@ +/* + * 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.ui; + +import com.android.gallery3d.ui.PositionRepository.Position; + +public interface PositionProvider { + public Position getPosition(long identity, Position target); +} diff --git a/src/com/android/gallery3d/ui/PositionRepository.java b/src/com/android/gallery3d/ui/PositionRepository.java new file mode 100644 index 000000000..0b829fa25 --- /dev/null +++ b/src/com/android/gallery3d/ui/PositionRepository.java @@ -0,0 +1,139 @@ +/* + * 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.ui; + +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.common.Utils; + +import java.util.HashMap; +import java.util.WeakHashMap; + +public class PositionRepository { + private static final WeakHashMap<GalleryActivity, PositionRepository> + sMap = new WeakHashMap<GalleryActivity, PositionRepository>(); + + public static class Position implements Cloneable { + public float x; + public float y; + public float z; + public float theta; + public float alpha; + + public Position() { + } + + public Position(float x, float y, float z) { + this(x, y, z, 0f, 1f); + } + + public Position(float x, float y, float z, float ftheta, float alpha) { + this.x = x; + this.y = y; + this.z = z; + this.theta = ftheta; + this.alpha = alpha; + } + + @Override + public Position clone() { + try { + return (Position) super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(); // we do support clone. + } + } + + public void set(Position another) { + x = another.x; + y = another.y; + z = another.z; + theta = another.theta; + alpha = another.alpha; + } + + public void set(float x, float y, float z, float ftheta, float alpha) { + this.x = x; + this.y = y; + this.z = z; + this.theta = ftheta; + this.alpha = alpha; + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof Position)) return false; + Position position = (Position) object; + return x == position.x && y == position.y && z == position.z + && theta == position.theta + && alpha == position.alpha; + } + + public static void interpolate( + Position source, Position target, Position output, float progress) { + if (progress < 1f) { + output.set( + Utils.interpolateScale(source.x, target.x, progress), + Utils.interpolateScale(source.y, target.y, progress), + Utils.interpolateScale(source.z, target.z, progress), + Utils.interpolateAngle(source.theta, target.theta, progress), + Utils.interpolateScale(source.alpha, target.alpha, progress)); + } else { + output.set(target); + } + } + } + + public static PositionRepository getInstance(GalleryActivity activity) { + PositionRepository repository = sMap.get(activity); + if (repository == null) { + repository = new PositionRepository(); + sMap.put(activity, repository); + } + return repository; + } + + private HashMap<Long, Position> mData = new HashMap<Long, Position>(); + private int mOffsetX; + private int mOffsetY; + private Position mTempPosition = new Position(); + + public Position get(Long identity) { + Position position = mData.get(identity); + if (position == null) return null; + mTempPosition.set(position); + position = mTempPosition; + position.x -= mOffsetX; + position.y -= mOffsetY; + return position; + } + + public void setOffset(int offsetX, int offsetY) { + mOffsetX = offsetX; + mOffsetY = offsetY; + } + + public void putPosition(Long identity, Position position) { + Position clone = position.clone(); + clone.x += mOffsetX; + clone.y += mOffsetY; + mData.put(identity, clone); + } + + public void clear() { + mData.clear(); + } +} diff --git a/src/com/android/gallery3d/ui/ProgressBar.java b/src/com/android/gallery3d/ui/ProgressBar.java new file mode 100644 index 000000000..c62fa9a62 --- /dev/null +++ b/src/com/android/gallery3d/ui/ProgressBar.java @@ -0,0 +1,65 @@ +/* + * 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.ui; + +import android.content.Context; +import android.graphics.Rect; + +public class ProgressBar extends GLView { + private final int MAX_PROGRESS = 10000; + private int mProgress; + private int mSecondaryProgress; + private BasicTexture mProgressTexture; + private BasicTexture mSecondaryProgressTexture; + private BasicTexture mBackgrondTexture; + + + public ProgressBar(Context context, int resProgress, + int resSecondaryProgress, int resBackground) { + mProgressTexture = new NinePatchTexture(context, resProgress); + mSecondaryProgressTexture = new NinePatchTexture( + context, resSecondaryProgress); + mBackgrondTexture = new NinePatchTexture(context, resBackground); + + } + + // The progress value is between 0 (empty) and MAX_PROGRESS (full). + public void setProgress(int progress) { + mProgress = progress; + } + + public void setSecondaryProgress(int progress) { + mSecondaryProgress = progress; + } + + @Override + protected void render(GLCanvas canvas) { + Rect p = mPaddings; + + int width = getWidth() - p.left - p.right; + int height = getHeight() - p.top - p.bottom; + + int primary = width * mProgress / MAX_PROGRESS; + int secondary = width * mSecondaryProgress / MAX_PROGRESS; + int x = p.left; + int y = p.top; + + canvas.drawTexture(mBackgrondTexture, x, y, width, height); + canvas.drawTexture(mProgressTexture, x, y, primary, height); + canvas.drawTexture(mSecondaryProgressTexture, x, y, secondary, height); + } +} diff --git a/src/com/android/gallery3d/ui/ProgressSpinner.java b/src/com/android/gallery3d/ui/ProgressSpinner.java new file mode 100644 index 000000000..e4d60242b --- /dev/null +++ b/src/com/android/gallery3d/ui/ProgressSpinner.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.ui; + +import com.android.gallery3d.R; + +import android.content.Context; + +public class ProgressSpinner { + private static float ROTATE_SPEED_OUTER = 1080f / 3500f; + private static float ROTATE_SPEED_INNER = -720f / 3500f; + private final ResourceTexture mOuter; + private final ResourceTexture mInner; + private final int mWidth; + private final int mHeight; + + private float mInnerDegree = 0f; + private float mOuterDegree = 0f; + private long mAnimationTimestamp = -1; + + public ProgressSpinner(Context context) { + mOuter = new ResourceTexture(context, R.drawable.spinner_76_outer_holo); + mInner = new ResourceTexture(context, R.drawable.spinner_76_inner_holo); + + mWidth = Math.max(mOuter.getWidth(), mInner.getWidth()); + mHeight = Math.max(mOuter.getHeight(), mInner.getHeight()); + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + + public void startAnimation() { + mAnimationTimestamp = -1; + mOuterDegree = 0; + mInnerDegree = 0; + } + + public void draw(GLCanvas canvas, int x, int y) { + long now = canvas.currentAnimationTimeMillis(); + if (mAnimationTimestamp == -1) mAnimationTimestamp = now; + mOuterDegree += (now - mAnimationTimestamp) * ROTATE_SPEED_OUTER; + mInnerDegree += (now - mAnimationTimestamp) * ROTATE_SPEED_INNER; + + mAnimationTimestamp = now; + + // just preventing overflow + if (mOuterDegree > 360) mOuterDegree -= 360f; + if (mInnerDegree < 0) mInnerDegree += 360f; + + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + + canvas.translate(x + mWidth / 2, y + mHeight / 2, 0); + canvas.rotate(mInnerDegree, 0, 0, 1); + mOuter.draw(canvas, -mOuter.getWidth() / 2, -mOuter.getHeight() / 2); + canvas.rotate(mOuterDegree - mInnerDegree, 0, 0, 1); + mInner.draw(canvas, -mInner.getWidth() / 2, -mInner.getHeight() / 2); + canvas.restore(); + } +} diff --git a/src/com/android/gallery3d/ui/RawTexture.java b/src/com/android/gallery3d/ui/RawTexture.java new file mode 100644 index 000000000..c1be435d1 --- /dev/null +++ b/src/com/android/gallery3d/ui/RawTexture.java @@ -0,0 +1,54 @@ +/* + * 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.ui; + +import javax.microedition.khronos.opengles.GL11; + +// RawTexture is used for texture created by glCopyTexImage2D. +// +// It will throw RuntimeException in onBind() if used with a different GL +// context. It is only used internally by copyTexture() in GLCanvas. +class RawTexture extends BasicTexture { + + private RawTexture(GLCanvas canvas, int id) { + super(canvas, id, STATE_LOADED); + } + + public static RawTexture newInstance(GLCanvas canvas) { + int[] textureId = new int[1]; + GL11 gl = canvas.getGLInstance(); + gl.glGenTextures(1, textureId, 0); + return new RawTexture(canvas, textureId[0]); + } + + @Override + protected boolean onBind(GLCanvas canvas) { + if (mCanvasRef.get() != canvas) { + throw new RuntimeException("cannot bind to different canvas"); + } + return true; + } + + public boolean isOpaque() { + return true; + } + + @Override + public void yield() { + // we cannot free the texture because we have no backup. + } +} diff --git a/src/com/android/gallery3d/ui/ResourceTexture.java b/src/com/android/gallery3d/ui/ResourceTexture.java new file mode 100644 index 000000000..08fb89187 --- /dev/null +++ b/src/com/android/gallery3d/ui/ResourceTexture.java @@ -0,0 +1,52 @@ +/* + * 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.ui; + +import com.android.gallery3d.common.Utils; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +// ResourceTexture is a texture whose Bitmap is decoded from a resource. +// By default ResourceTexture is not opaque. +public class ResourceTexture extends UploadedTexture { + + protected final Context mContext; + protected final int mResId; + + public ResourceTexture(Context context, int resId) { + mContext = Utils.checkNotNull(context); + mResId = resId; + setOpaque(false); + } + + @Override + protected Bitmap onGetBitmap() { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + return BitmapFactory.decodeResource( + mContext.getResources(), mResId, options); + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + if (!inFinalizer()) { + bitmap.recycle(); + } + } +} diff --git a/src/com/android/gallery3d/ui/ScrollBarView.java b/src/com/android/gallery3d/ui/ScrollBarView.java new file mode 100644 index 000000000..7e375c9f7 --- /dev/null +++ b/src/com/android/gallery3d/ui/ScrollBarView.java @@ -0,0 +1,135 @@ +/* + * 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.ui; + +import com.android.gallery3d.R; + +import android.content.Context; +import android.graphics.Rect; + +public class ScrollBarView extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "ScrollBarView"; + + public interface Listener { + void onScrollBarPositionChanged(int position); + } + + private int mBarHeight; + + private int mGripHeight; + private int mGripPosition; // left side of the grip + private int mGripWidth; // zero if the grip is disabled + private int mGivenGripWidth; + + private int mContentPosition; + private int mContentTotal; + + private Listener mListener; + private NinePatchTexture mScrollBarTexture; + + public ScrollBarView(Context context, int gripHeight, int gripWidth) { + mScrollBarTexture = new NinePatchTexture( + context, R.drawable.scrollbar_handle_holo_dark); + mGripPosition = 0; + mGripWidth = 0; + mGivenGripWidth = gripWidth; + mGripHeight = gripHeight; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + if (!changed) return; + mBarHeight = bottom - top; + } + + // The content position is between 0 to "total". The current position is + // in "position". + public void setContentPosition(int position, int total) { + if (position == mContentPosition && total == mContentTotal) { + return; + } + + invalidate(); + + mContentPosition = position; + mContentTotal = total; + + // If the grip cannot move, don't draw it. + if (mContentTotal <= 0) { + mGripPosition = 0; + mGripWidth = 0; + return; + } + + // Map from the content range to scroll bar range. + // + // mContentTotal --> getWidth() - mGripWidth + // mContentPosition --> mGripPosition + mGripWidth = mGivenGripWidth; + float r = (getWidth() - mGripWidth) / (float) mContentTotal; + mGripPosition = Math.round(r * mContentPosition); + } + + private void notifyContentPositionFromGrip() { + if (mContentTotal <= 0) return; + float r = (getWidth() - mGripWidth) / (float) mContentTotal; + int newContentPosition = Math.round(mGripPosition / r); + mListener.onScrollBarPositionChanged(newContentPosition); + } + + @Override + protected void render(GLCanvas canvas) { + super.render(canvas); + if (mGripWidth == 0) return; + Rect b = bounds(); + int y = (mBarHeight - mGripHeight) / 2; + mScrollBarTexture.draw(canvas, mGripPosition, y, mGripWidth, mGripHeight); + } + + // The onTouch() handler is disabled because now we don't want the user + // to drag the bar (it's an indicator only). + /* + @Override + protected boolean onTouch(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + int x = (int) event.getX(); + return (x >= mGripPosition && x < mGripPosition + mGripWidth); + } + case MotionEvent.ACTION_MOVE: { + // Adjust x by mGripWidth / 2 so the center of the grip + // matches the touch position. + int x = (int) event.getX() - mGripWidth / 2; + x = Utils.clamp(x, 0, getWidth() - mGripWidth); + if (mGripPosition != x) { + mGripPosition = x; + notifyContentPositionFromGrip(); + invalidate(); + } + break; + } + } + return true; + } + */ +} diff --git a/src/com/android/gallery3d/ui/ScrollView.java b/src/com/android/gallery3d/ui/ScrollView.java new file mode 100644 index 000000000..f7628335c --- /dev/null +++ b/src/com/android/gallery3d/ui/ScrollView.java @@ -0,0 +1,99 @@ +/* + * 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.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; + +import android.content.Context; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View.MeasureSpec; + +// The current implementation can only scroll vertically. +public class ScrollView extends GLView { + + private static final int MIN_SCROLLER_HEIGHT = 20; + + private NinePatchTexture mScroller; + private int mScrollLimit = 0; + private int mScrollerHeight = MIN_SCROLLER_HEIGHT; + private GestureDetector mGestureDetector; + + public ScrollView(Context context) { + mScroller = new NinePatchTexture(context, R.drawable.scrollbar_handle_holo_dark); + mGestureDetector = new GestureDetector(context, new MyGestureListener()); + } + + private GLView getContentView() { + return getComponentCount() == 0 ? null : getComponent(0); + } + + @Override + public void onLayout(boolean sizeChange, int l, int t, int r, int b) { + GLView content = getContentView(); + int width = getWidth(); + int height = getHeight(); + content.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.UNSPECIFIED); + int contentHeight = content.getMeasuredHeight(); + content.layout(0, 0, width, contentHeight); + if (height < contentHeight) { + mScrollLimit = contentHeight - height; + mScrollerHeight = Math.max(MIN_SCROLLER_HEIGHT, + height * height / contentHeight); + } else { + mScrollLimit = 0; + } + mScrollY = Utils.clamp(mScrollY, 0, mScrollLimit); + } + + @Override + public void render(GLCanvas canvas) { + GLView content = getContentView(); + if (content == null) return; + int width = getWidth(); + int height = getHeight(); + + canvas.save(GLCanvas.SAVE_FLAG_CLIP); + canvas.clipRect(0, 0, width, height); + super.render(canvas); + if (mScrollLimit > 0) { + int x = getWidth() - mScroller.getWidth(); + int y = (height - mScrollerHeight) * mScrollY / mScrollLimit; + mScroller.draw(canvas, x, y, mScroller.getWidth(), mScrollerHeight); + } + canvas.restore(); + } + + @Override + public boolean onTouch(MotionEvent event) { + mGestureDetector.onTouchEvent(event); + return true; + } + + private class MyGestureListener + extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onScroll(MotionEvent e1, + MotionEvent e2, float distanceX, float distanceY) { + mScrollY = Utils.clamp(mScrollY + (int) distanceY, 0, mScrollLimit); + invalidate(); + return true; + } + } +} diff --git a/src/com/android/gallery3d/ui/ScrollerHelper.java b/src/com/android/gallery3d/ui/ScrollerHelper.java new file mode 100644 index 000000000..9f19cec96 --- /dev/null +++ b/src/com/android/gallery3d/ui/ScrollerHelper.java @@ -0,0 +1,93 @@ +/* + * 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.ui; + +import com.android.gallery3d.common.Utils; + +import android.content.Context; +import android.view.ViewConfiguration; +import android.widget.OverScroller; + +public class ScrollerHelper { + private OverScroller mScroller; + private int mOverflingDistance; + private boolean mOverflingEnabled; + + public ScrollerHelper(Context context) { + mScroller = new OverScroller(context); + ViewConfiguration configuration = ViewConfiguration.get(context); + mOverflingDistance = configuration.getScaledOverflingDistance(); + } + + public void setOverfling(boolean enabled) { + mOverflingEnabled = enabled; + } + + /** + * Call this when you want to know the new location. The position will be + * updated and can be obtained by getPosition(). Returns true if the + * animation is not yet finished. + */ + public boolean advanceAnimation(long currentTimeMillis) { + return mScroller.computeScrollOffset(); + } + + public boolean isFinished() { + return mScroller.isFinished(); + } + + public void forceFinished() { + mScroller.forceFinished(true); + } + + public int getPosition() { + return mScroller.getCurrX(); + } + + public void setPosition(int position) { + mScroller.startScroll( + position, 0, // startX, startY + 0, 0, 0); // dx, dy, duration + + // This forces the scroller to reach the final position. + mScroller.abortAnimation(); + } + + public void fling(int velocity, int min, int max) { + int currX = getPosition(); + mScroller.fling( + currX, 0, // startX, startY + velocity, 0, // velocityX, velocityY + min, max, // minX, maxX + 0, 0, // minY, maxY + mOverflingEnabled ? mOverflingDistance : 0, 0); + } + + public boolean startScroll(int distance, int min, int max) { + int currPosition = mScroller.getCurrX(); + int finalPosition = mScroller.getFinalX(); + int newPosition = Utils.clamp(finalPosition + distance, min, max); + if (newPosition != currPosition) { + mScroller.startScroll( + currPosition, 0, // startX, startY + newPosition - currPosition, 0, 0); // dx, dy, duration + return true; + } else { + return false; + } + } +} diff --git a/src/com/android/gallery3d/ui/SelectionDrawer.java b/src/com/android/gallery3d/ui/SelectionDrawer.java new file mode 100644 index 000000000..2655a221c --- /dev/null +++ b/src/com/android/gallery3d/ui/SelectionDrawer.java @@ -0,0 +1,89 @@ +/* + * 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.ui; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.Path; + +import android.graphics.Rect; + +/** + * Drawer class responsible for drawing selectable frame. + */ +public abstract class SelectionDrawer { + public static final int DATASOURCE_TYPE_NOT_CATEGORIZED = 0; + public static final int DATASOURCE_TYPE_LOCAL = 1; + public static final int DATASOURCE_TYPE_PICASA = 2; + public static final int DATASOURCE_TYPE_MTP = 3; + public static final int DATASOURCE_TYPE_CAMERA = 4; + + public abstract void prepareDrawing(); + public abstract void draw(GLCanvas canvas, Texture content, + int width, int height, int rotation, Path path, + int topIndex, int dataSourceType, int mediaType, + boolean wantCache, boolean isCaching); + public abstract void drawFocus(GLCanvas canvas, int width, int height); + + public void draw(GLCanvas canvas, Texture content, int width, int height, + int rotation, Path path, int mediaType) { + draw(canvas, content, width, height, rotation, path, 0, + DATASOURCE_TYPE_NOT_CATEGORIZED, mediaType, + false, false); + } + + public static void drawWithRotation(GLCanvas canvas, Texture content, + int x, int y, int width, int height, int rotation) { + if (rotation != 0) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + canvas.rotate(rotation, 0, 0, 1); + } + + content.draw(canvas, x, y, width, height); + + if (rotation != 0) { + canvas.restore(); + } + } + + public static void drawWithRotationAndGray(GLCanvas canvas, Texture content, + int x, int y, int width, int height, int rotation, + int topIndex) { + if (rotation != 0) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + canvas.rotate(rotation, 0, 0, 1); + } + + if (topIndex > 0 && (content instanceof BasicTexture)) { + float ratio = Utils.clamp(0.3f + 0.2f * topIndex, 0f, 1f); + canvas.drawMixed((BasicTexture) content, 0xFF222222, ratio, + x, y, width, height); + } else { + content.draw(canvas, x, y, width, height); + } + + if (rotation != 0) { + canvas.restore(); + } + } + + public static void drawFrame(GLCanvas canvas, NinePatchTexture frame, + int x, int y, int width, int height) { + Rect p = frame.getPaddings(); + frame.draw(canvas, x - p.left, y - p.top, width + p.left + p.right, + height + p.top + p.bottom); + } +} diff --git a/src/com/android/gallery3d/ui/SelectionManager.java b/src/com/android/gallery3d/ui/SelectionManager.java new file mode 100644 index 000000000..b85ca7a41 --- /dev/null +++ b/src/com/android/gallery3d/ui/SelectionManager.java @@ -0,0 +1,221 @@ +/* + * 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.ui; + +import com.android.gallery3d.app.GalleryContext; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; + +import android.content.Context; +import android.os.Vibrator; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +public class SelectionManager { + @SuppressWarnings("unused") + private static final String TAG = "SelectionManager"; + + public static final int ENTER_SELECTION_MODE = 1; + public static final int LEAVE_SELECTION_MODE = 2; + public static final int SELECT_ALL_MODE = 3; + + private Set<Path> mClickedSet; + private MediaSet mSourceMediaSet; + private final Vibrator mVibrator; + private SelectionListener mListener; + private DataManager mDataManager; + private boolean mInverseSelection; + private boolean mIsAlbumSet; + private boolean mInSelectionMode; + private boolean mAutoLeave = true; + private int mTotal; + + public interface SelectionListener { + public void onSelectionModeChange(int mode); + public void onSelectionChange(Path path, boolean selected); + } + + public SelectionManager(GalleryContext galleryContext, boolean isAlbumSet) { + Context context = galleryContext.getAndroidContext(); + mDataManager = galleryContext.getDataManager(); + mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + mClickedSet = new HashSet<Path>(); + mIsAlbumSet = isAlbumSet; + mTotal = -1; + } + + // Whether we will leave selection mode automatically once the number of + // selected items is down to zero. + public void setAutoLeaveSelectionMode(boolean enable) { + mAutoLeave = enable; + } + + public void setSelectionListener(SelectionListener listener) { + mListener = listener; + } + + public void selectAll() { + enterSelectionMode(); + mInverseSelection = true; + mClickedSet.clear(); + if (mListener != null) mListener.onSelectionModeChange(SELECT_ALL_MODE); + } + + public void deSelectAll() { + leaveSelectionMode(); + mInverseSelection = false; + mClickedSet.clear(); + } + + public boolean inSelectAllMode() { + return mInverseSelection; + } + + public boolean inSelectionMode() { + return mInSelectionMode; + } + + public void enterSelectionMode() { + if (mInSelectionMode) return; + + mInSelectionMode = true; + mVibrator.vibrate(100); + if (mListener != null) mListener.onSelectionModeChange(ENTER_SELECTION_MODE); + } + + public void leaveSelectionMode() { + if (!mInSelectionMode) return; + + mInSelectionMode = false; + mInverseSelection = false; + mClickedSet.clear(); + if (mListener != null) mListener.onSelectionModeChange(LEAVE_SELECTION_MODE); + } + + public boolean isItemSelected(Path itemId) { + return mInverseSelection ^ mClickedSet.contains(itemId); + } + + public int getSelectedCount() { + int count = mClickedSet.size(); + if (mInverseSelection) { + if (mTotal < 0) { + mTotal = mIsAlbumSet + ? mSourceMediaSet.getSubMediaSetCount() + : mSourceMediaSet.getMediaItemCount(); + } + count = mTotal - count; + } + return count; + } + + public void toggle(Path path) { + if (mClickedSet.contains(path)) { + mClickedSet.remove(path); + } else { + enterSelectionMode(); + mClickedSet.add(path); + } + + if (mListener != null) mListener.onSelectionChange(path, isItemSelected(path)); + if (getSelectedCount() == 0 && mAutoLeave) { + leaveSelectionMode(); + } + } + + private static void expandMediaSet(ArrayList<Path> items, MediaSet set) { + int subCount = set.getSubMediaSetCount(); + for (int i = 0; i < subCount; i++) { + expandMediaSet(items, set.getSubMediaSet(i)); + } + int total = set.getMediaItemCount(); + int batch = 50; + int index = 0; + + while (index < total) { + int count = index + batch < total + ? batch + : total - index; + ArrayList<MediaItem> list = set.getMediaItem(index, count); + for (MediaItem item : list) { + items.add(item.getPath()); + } + index += batch; + } + } + + public ArrayList<Path> getSelected(boolean expandSet) { + ArrayList<Path> selected = new ArrayList<Path>(); + if (mIsAlbumSet) { + if (mInverseSelection) { + int max = mSourceMediaSet.getSubMediaSetCount(); + for (int i = 0; i < max; i++) { + MediaSet set = mSourceMediaSet.getSubMediaSet(i); + Path id = set.getPath(); + if (!mClickedSet.contains(id)) { + if (expandSet) { + expandMediaSet(selected, set); + } else { + selected.add(id); + } + } + } + } else { + for (Path id : mClickedSet) { + if (expandSet) { + expandMediaSet(selected, mDataManager.getMediaSet(id)); + } else { + selected.add(id); + } + } + } + } else { + if (mInverseSelection) { + + int total = mSourceMediaSet.getMediaItemCount(); + int index = 0; + while (index < total) { + int count = Math.min(total - index, MediaSet.MEDIAITEM_BATCH_FETCH_COUNT); + ArrayList<MediaItem> list = mSourceMediaSet.getMediaItem(index, count); + for (MediaItem item : list) { + Path id = item.getPath(); + if (!mClickedSet.contains(id)) selected.add(id); + } + index += count; + } + } else { + for (Path id : mClickedSet) { + selected.add(id); + } + } + } + return selected; + } + + public void setSourceMediaSet(MediaSet set) { + mSourceMediaSet = set; + mTotal = -1; + } + + public MediaSet getSourceMediaSet() { + return mSourceMediaSet; + } +} diff --git a/src/com/android/gallery3d/ui/SlideshowView.java b/src/com/android/gallery3d/ui/SlideshowView.java new file mode 100644 index 000000000..79a6bf080 --- /dev/null +++ b/src/com/android/gallery3d/ui/SlideshowView.java @@ -0,0 +1,165 @@ +/* + * 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.ui; + +import com.android.gallery3d.anim.CanvasAnimation; +import com.android.gallery3d.anim.FloatAnimation; + +import android.graphics.Bitmap; +import android.graphics.PointF; + +import java.util.Random; +import javax.microedition.khronos.opengles.GL11; + +public class SlideshowView extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "SlideshowView"; + + private static final int SLIDESHOW_DURATION = 3500; + private static final int TRANSITION_DURATION = 1000; + + private static final float SCALE_SPEED = 0.20f ; + private static final float MOVE_SPEED = SCALE_SPEED; + + private int mCurrentRotation; + private BitmapTexture mCurrentTexture; + private SlideshowAnimation mCurrentAnimation; + + private int mPrevRotation; + private BitmapTexture mPrevTexture; + private SlideshowAnimation mPrevAnimation; + + private final FloatAnimation mTransitionAnimation = + new FloatAnimation(0, 1, TRANSITION_DURATION); + + private Random mRandom = new Random(); + + public void next(Bitmap bitmap, int rotation) { + + mTransitionAnimation.start(); + + if (mPrevTexture != null) { + mPrevTexture.getBitmap().recycle(); + mPrevTexture.recycle(); + } + + mPrevTexture = mCurrentTexture; + mPrevAnimation = mCurrentAnimation; + mPrevRotation = mCurrentRotation; + + mCurrentRotation = rotation; + mCurrentTexture = new BitmapTexture(bitmap); + if (((rotation / 90) & 0x01) == 0) { + mCurrentAnimation = new SlideshowAnimation( + mCurrentTexture.getWidth(), mCurrentTexture.getHeight(), + mRandom); + } else { + mCurrentAnimation = new SlideshowAnimation( + mCurrentTexture.getHeight(), mCurrentTexture.getWidth(), + mRandom); + } + mCurrentAnimation.start(); + + invalidate(); + } + + public void release() { + if (mPrevTexture != null) { + mPrevTexture.recycle(); + mPrevTexture = null; + } + if (mCurrentTexture != null) { + mCurrentTexture.recycle(); + mCurrentTexture = null; + } + } + + @Override + protected void render(GLCanvas canvas) { + long currentTimeMillis = canvas.currentAnimationTimeMillis(); + boolean requestRender = mTransitionAnimation.calculate(currentTimeMillis); + GL11 gl = canvas.getGLInstance(); + gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE); + float alpha = mPrevTexture == null ? 1f : mTransitionAnimation.get(); + + if (mPrevTexture != null && alpha != 1f) { + requestRender |= mPrevAnimation.calculate(currentTimeMillis); + canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX); + canvas.setAlpha(1f - alpha); + mPrevAnimation.apply(canvas); + canvas.rotate(mPrevRotation, 0, 0, 1); + mPrevTexture.draw(canvas, -mPrevTexture.getWidth() / 2, + -mPrevTexture.getHeight() / 2); + canvas.restore(); + } + if (mCurrentTexture != null) { + requestRender |= mCurrentAnimation.calculate(currentTimeMillis); + canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX); + canvas.setAlpha(alpha); + mCurrentAnimation.apply(canvas); + canvas.rotate(mCurrentRotation, 0, 0, 1); + mCurrentTexture.draw(canvas, -mCurrentTexture.getWidth() / 2, + -mCurrentTexture.getHeight() / 2); + canvas.restore(); + } + if (requestRender) invalidate(); + gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA); + } + + private class SlideshowAnimation extends CanvasAnimation { + private final int mWidth; + private final int mHeight; + + private final PointF mMovingVector; + private float mProgress; + + public SlideshowAnimation(int width, int height, Random random) { + mWidth = width; + mHeight = height; + mMovingVector = new PointF( + MOVE_SPEED * mWidth * (random.nextFloat() - 0.5f), + MOVE_SPEED * mHeight * (random.nextFloat() - 0.5f)); + setDuration(SLIDESHOW_DURATION); + } + + @Override + public void apply(GLCanvas canvas) { + int viewWidth = getWidth(); + int viewHeight = getHeight(); + + float initScale = Math.min(2f, Math.min((float) + viewWidth / mWidth, (float) viewHeight / mHeight)); + float scale = initScale * (1 + SCALE_SPEED * mProgress); + + float centerX = viewWidth / 2 + mMovingVector.x * mProgress; + float centerY = viewHeight / 2 + mMovingVector.y * mProgress; + + canvas.translate(centerX, centerY, 0); + canvas.scale(scale, scale, 0); + } + + @Override + public int getCanvasSaveFlags() { + return GLCanvas.SAVE_FLAG_MATRIX; + } + + @Override + protected void onCalculate(float progress) { + mProgress = progress; + } + } +} diff --git a/src/com/android/gallery3d/ui/SlotView.java b/src/com/android/gallery3d/ui/SlotView.java new file mode 100644 index 000000000..a8ca5f290 --- /dev/null +++ b/src/com/android/gallery3d/ui/SlotView.java @@ -0,0 +1,607 @@ +/* + * 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.ui; + +import com.android.gallery3d.anim.Animation; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.ui.PositionRepository.Position; +import com.android.gallery3d.util.LinkedNode; + +import android.content.Context; +import android.graphics.Rect; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.animation.DecelerateInterpolator; + +import java.util.ArrayList; +import java.util.HashMap; + +public class SlotView extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "SlotView"; + + private static final boolean WIDE = true; + + private static final int INDEX_NONE = -1; + + public interface Listener { + public void onSingleTapUp(int index); + public void onLongTap(int index); + public void onScrollPositionChanged(int position, int total); + } + + public static class SimpleListener implements Listener { + public void onSingleTapUp(int index) {} + public void onLongTap(int index) {} + public void onScrollPositionChanged(int position, int total) {} + } + + private final GestureDetector mGestureDetector; + private final ScrollerHelper mScroller; + private final Paper mPaper = new Paper(); + + private Listener mListener; + private UserInteractionListener mUIListener; + + // Use linked hash map to keep the rendering order + private HashMap<DisplayItem, ItemEntry> mItems = + new HashMap<DisplayItem, ItemEntry>(); + + public LinkedNode.List<ItemEntry> mItemList = LinkedNode.newList(); + + // This is used for multipass rendering + private ArrayList<ItemEntry> mCurrentItems = new ArrayList<ItemEntry>(); + private ArrayList<ItemEntry> mNextItems = new ArrayList<ItemEntry>(); + + private boolean mMoreAnimation = false; + private MyAnimation mAnimation = null; + private final Position mTempPosition = new Position(); + private final Layout mLayout = new Layout(); + private PositionProvider mPositions; + private int mStartIndex = INDEX_NONE; + + // whether the down action happened while the view is scrolling. + private boolean mDownInScrolling; + private int mOverscrollEffect = OVERSCROLL_3D; + + public static final int OVERSCROLL_3D = 0; + public static final int OVERSCROLL_SYSTEM = 1; + public static final int OVERSCROLL_NONE = 2; + + public SlotView(Context context) { + mGestureDetector = + new GestureDetector(context, new MyGestureListener()); + mScroller = new ScrollerHelper(context); + } + + public void setCenterIndex(int index) { + int slotCount = mLayout.mSlotCount; + if (index < 0 || index >= slotCount) { + return; + } + Rect rect = mLayout.getSlotRect(index); + int position = WIDE + ? (rect.left + rect.right - getWidth()) / 2 + : (rect.top + rect.bottom - getHeight()) / 2; + setScrollPosition(position); + } + + public void makeSlotVisible(int index) { + Rect rect = mLayout.getSlotRect(index); + int visibleBegin = WIDE ? mScrollX : mScrollY; + int visibleLength = WIDE ? getWidth() : getHeight(); + int visibleEnd = visibleBegin + visibleLength; + int slotBegin = WIDE ? rect.left : rect.top; + int slotEnd = WIDE ? rect.right : rect.bottom; + + int position = visibleBegin; + if (visibleLength < slotEnd - slotBegin) { + position = visibleBegin; + } else if (slotBegin < visibleBegin) { + position = slotBegin; + } else if (slotEnd > visibleEnd) { + position = slotEnd - visibleLength; + } + + setScrollPosition(position); + } + + public void setScrollPosition(int position) { + position = Utils.clamp(position, 0, mLayout.getScrollLimit()); + mScroller.setPosition(position); + updateScrollPosition(position, false); + } + + public void setSlotSize(int slotWidth, int slotHeight) { + mLayout.setSlotSize(slotWidth, slotHeight); + } + + @Override + public void addComponent(GLView view) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeComponent(GLView view) { + throw new UnsupportedOperationException(); + } + + @Override + protected void onLayout(boolean changeSize, int l, int t, int r, int b) { + if (!changeSize) return; + mLayout.setSize(r - l, b - t); + onLayoutChanged(r - l, b - t); + if (mOverscrollEffect == OVERSCROLL_3D) { + mPaper.setSize(r - l, b - t); + } + } + + protected void onLayoutChanged(int width, int height) { + } + + public void startTransition(PositionProvider position) { + mPositions = position; + mAnimation = new MyAnimation(); + mAnimation.start(); + if (mItems.size() != 0) invalidate(); + } + + public void savePositions(PositionRepository repository) { + repository.clear(); + LinkedNode.List<ItemEntry> list = mItemList; + ItemEntry entry = list.getFirst(); + Position position = new Position(); + while (entry != null) { + position.set(entry.target); + position.x -= mScrollX; + position.y -= mScrollY; + repository.putPosition(entry.item.getIdentity(), position); + entry = list.nextOf(entry); + } + } + + private void updateScrollPosition(int position, boolean force) { + if (!force && (WIDE ? position == mScrollX : position == mScrollY)) return; + if (WIDE) { + mScrollX = position; + } else { + mScrollY = position; + } + mLayout.setScrollPosition(position); + onScrollPositionChanged(position); + } + + protected void onScrollPositionChanged(int newPosition) { + int limit = mLayout.getScrollLimit(); + mListener.onScrollPositionChanged(newPosition, limit); + } + + public void putDisplayItem(Position target, Position base, DisplayItem item) { + ItemEntry entry = new ItemEntry(item, target, base); + mItemList.insertLast(entry); + mItems.put(item, entry); + } + + public void removeDisplayItem(DisplayItem item) { + ItemEntry entry = mItems.remove(item); + if (entry != null) entry.remove(); + } + + public Rect getSlotRect(int slotIndex) { + return mLayout.getSlotRect(slotIndex); + } + + @Override + protected boolean onTouch(MotionEvent event) { + if (mUIListener != null) mUIListener.onUserInteraction(); + mGestureDetector.onTouchEvent(event); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mDownInScrolling = !mScroller.isFinished(); + mScroller.forceFinished(); + break; + } + return true; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public void setUserInteractionListener(UserInteractionListener listener) { + mUIListener = listener; + } + + public void setOverscrollEffect(int kind) { + mOverscrollEffect = kind; + mScroller.setOverfling(kind == OVERSCROLL_SYSTEM); + } + + @Override + protected void render(GLCanvas canvas) { + canvas.save(GLCanvas.SAVE_FLAG_CLIP); + canvas.clipRect(0, 0, getWidth(), getHeight()); + super.render(canvas); + + long currentTimeMillis = canvas.currentAnimationTimeMillis(); + boolean more = mScroller.advanceAnimation(currentTimeMillis); + boolean paperActive = (mOverscrollEffect == OVERSCROLL_3D) + && mPaper.advanceAnimation(currentTimeMillis); + updateScrollPosition(mScroller.getPosition(), false); + float interpolate = 1f; + if (mAnimation != null) { + more |= mAnimation.calculate(currentTimeMillis); + interpolate = mAnimation.value; + } + + more |= paperActive; + + if (WIDE) { + canvas.translate(-mScrollX, 0, 0); + } else { + canvas.translate(0, -mScrollY, 0); + } + + LinkedNode.List<ItemEntry> list = mItemList; + for (ItemEntry entry = list.getLast(); entry != null;) { + if (renderItem(canvas, entry, interpolate, 0, paperActive)) { + mCurrentItems.add(entry); + } + entry = list.previousOf(entry); + } + + int pass = 1; + while (!mCurrentItems.isEmpty()) { + for (int i = 0, n = mCurrentItems.size(); i < n; i++) { + ItemEntry entry = mCurrentItems.get(i); + if (renderItem(canvas, entry, interpolate, pass, paperActive)) { + mNextItems.add(entry); + } + } + mCurrentItems.clear(); + // swap mNextItems with mCurrentItems + ArrayList<ItemEntry> tmp = mNextItems; + mNextItems = mCurrentItems; + mCurrentItems = tmp; + pass += 1; + } + + if (WIDE) { + canvas.translate(mScrollX, 0, 0); + } else { + canvas.translate(0, mScrollY, 0); + } + + if (more) invalidate(); + if (mMoreAnimation && !more && mUIListener != null) { + mUIListener.onUserInteractionEnd(); + } + mMoreAnimation = more; + canvas.restore(); + } + + private boolean renderItem(GLCanvas canvas, ItemEntry entry, + float interpolate, int pass, boolean paperActive) { + canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX); + Position position = entry.target; + if (mPositions != null) { + position = mTempPosition; + position.set(entry.target); + position.x -= mScrollX; + position.y -= mScrollY; + Position source = mPositions + .getPosition(entry.item.getIdentity(), position); + source.x += mScrollX; + source.y += mScrollY; + position = mTempPosition; + Position.interpolate( + source, entry.target, position, interpolate); + } + canvas.multiplyAlpha(position.alpha); + if (paperActive) { + canvas.multiplyMatrix(mPaper.getTransform( + position, entry.base, mScrollX, mScrollY), 0); + } else { + canvas.translate(position.x, position.y, position.z); + } + canvas.rotate(position.theta, 0, 0, 1); + boolean more = entry.item.render(canvas, pass); + canvas.restore(); + return more; + } + + public static class MyAnimation extends Animation { + public float value; + + public MyAnimation() { + setInterpolator(new DecelerateInterpolator(4)); + setDuration(1500); + } + + @Override + protected void onCalculate(float progress) { + value = progress; + } + } + + private static class ItemEntry extends LinkedNode { + public DisplayItem item; + public Position target; + public Position base; + + public ItemEntry(DisplayItem item, Position target, Position base) { + this.item = item; + this.target = target; + this.base = base; + } + } + + public static class Layout { + + private int mVisibleStart; + private int mVisibleEnd; + + private int mSlotCount; + private int mSlotWidth; + private int mSlotHeight; + + private int mWidth; + private int mHeight; + + private int mUnitCount; + private int mContentLength; + private int mScrollPosition; + + private int mVerticalPadding; + private int mHorizontalPadding; + + public void setSlotSize(int slotWidth, int slotHeight) { + mSlotWidth = slotWidth; + mSlotHeight = slotHeight; + } + + public boolean setSlotCount(int slotCount) { + mSlotCount = slotCount; + int hPadding = mHorizontalPadding; + int vPadding = mVerticalPadding; + initLayoutParameters(); + return vPadding != mVerticalPadding || hPadding != mHorizontalPadding; + } + + public Rect getSlotRect(int index) { + int col, row; + if (WIDE) { + col = index / mUnitCount; + row = index - col * mUnitCount; + } else { + row = index / mUnitCount; + col = index - row * mUnitCount; + } + + int x = mHorizontalPadding + col * mSlotWidth; + int y = mVerticalPadding + row * mSlotHeight; + return new Rect(x, y, x + mSlotWidth, y + mSlotHeight); + } + + public int getContentLength() { + return mContentLength; + } + + // Calculate + // (1) mUnitCount: the number of slots we can fit into one column (or row). + // (2) mContentLength: the width (or height) we need to display all the + // columns (rows). + // (3) padding[]: the vertical and horizontal padding we need in order + // to put the slots towards to the center of the display. + // + // The "major" direction is the direction the user can scroll. The other + // direction is the "minor" direction. + // + // The comments inside this method are the description when the major + // directon is horizontal (X), and the minor directon is vertical (Y). + private void initLayoutParameters( + int majorLength, int minorLength, /* The view width and height */ + int majorUnitSize, int minorUnitSize, /* The slot width and height */ + int[] padding) { + int unitCount = minorLength / minorUnitSize; + if (unitCount == 0) unitCount = 1; + mUnitCount = unitCount; + + // We put extra padding above and below the column. + int availableUnits = Math.min(mUnitCount, mSlotCount); + padding[0] = (minorLength - availableUnits * minorUnitSize) / 2; + + // Then calculate how many columns we need for all slots. + int count = ((mSlotCount + mUnitCount - 1) / mUnitCount); + mContentLength = count * majorUnitSize; + + // If the content length is less then the screen width, put + // extra padding in left and right. + padding[1] = Math.max(0, (majorLength - mContentLength) / 2); + } + + private void initLayoutParameters() { + int[] padding = new int[2]; + if (WIDE) { + initLayoutParameters(mWidth, mHeight, mSlotWidth, mSlotHeight, padding); + mVerticalPadding = padding[0]; + mHorizontalPadding = padding[1]; + } else { + initLayoutParameters(mHeight, mWidth, mSlotHeight, mSlotWidth, padding); + mVerticalPadding = padding[1]; + mHorizontalPadding = padding[0]; + } + updateVisibleSlotRange(); + } + + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + initLayoutParameters(); + } + + private void updateVisibleSlotRange() { + int position = mScrollPosition; + + if (WIDE) { + int start = Math.max(0, (position / mSlotWidth) * mUnitCount); + int end = Math.min(mSlotCount, mUnitCount + * (position + mWidth + mSlotWidth - 1) / mSlotWidth); + setVisibleRange(start, end); + } else { + int start = Math.max(0, mUnitCount * (position / mSlotHeight)); + int end = Math.min(mSlotCount, mUnitCount + * (position + mHeight + mSlotHeight - 1) / mSlotHeight); + setVisibleRange(start, end); + } + } + + public void setScrollPosition(int position) { + if (mScrollPosition == position) return; + mScrollPosition = position; + updateVisibleSlotRange(); + } + + private void setVisibleRange(int start, int end) { + if (start == mVisibleStart && end == mVisibleEnd) return; + if (start < end) { + mVisibleStart = start; + mVisibleEnd = end; + } else { + mVisibleStart = mVisibleEnd = 0; + } + } + + public int getVisibleStart() { + return mVisibleStart; + } + + public int getVisibleEnd() { + return mVisibleEnd; + } + + public int getSlotIndexByPosition(float x, float y) { + float absoluteX = x + (WIDE ? mScrollPosition : 0); + absoluteX -= mHorizontalPadding; + int columnIdx = (int) (absoluteX + 0.5) / mSlotWidth; + if ((absoluteX - mSlotWidth * columnIdx) < 0 + || (!WIDE && columnIdx >= mUnitCount)) { + return INDEX_NONE; + } + + float absoluteY = y + (WIDE ? 0 : mScrollPosition); + absoluteY -= mVerticalPadding; + int rowIdx = (int) (absoluteY + 0.5) / mSlotHeight; + if (((absoluteY - mSlotHeight * rowIdx) < 0) + || (WIDE && rowIdx >= mUnitCount)) { + return INDEX_NONE; + } + int index = WIDE + ? (columnIdx * mUnitCount + rowIdx) + : (rowIdx * mUnitCount + columnIdx); + + return index >= mSlotCount ? INDEX_NONE : index; + } + + public int getScrollLimit() { + int limit = WIDE ? mContentLength - mWidth : mContentLength - mHeight; + return limit <= 0 ? 0 : limit; + } + } + + private class MyGestureListener + extends GestureDetector.SimpleOnGestureListener { + + @Override + public boolean onFling(MotionEvent e1, + MotionEvent e2, float velocityX, float velocityY) { + int scrollLimit = mLayout.getScrollLimit(); + if (scrollLimit == 0) return false; + float velocity = WIDE ? velocityX : velocityY; + mScroller.fling((int) -velocity, 0, scrollLimit); + if (mUIListener != null) mUIListener.onUserInteractionBegin(); + invalidate(); + return true; + } + + @Override + public boolean onScroll(MotionEvent e1, + MotionEvent e2, float distanceX, float distanceY) { + float distance = WIDE ? distanceX : distanceY; + boolean canMove = mScroller.startScroll( + Math.round(distance), 0, mLayout.getScrollLimit()); + if (mOverscrollEffect == OVERSCROLL_3D && !canMove) { + mPaper.overScroll(distance); + } + invalidate(); + return true; + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + if (mDownInScrolling) return true; + int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY()); + if (index != INDEX_NONE) mListener.onSingleTapUp(index); + return true; + } + + @Override + public void onLongPress(MotionEvent e) { + if (mDownInScrolling) return; + lockRendering(); + try { + int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY()); + if (index != INDEX_NONE) mListener.onLongTap(index); + } finally { + unlockRendering(); + } + } + } + + public void setStartIndex(int index) { + mStartIndex = index; + } + + // Return true if the layout parameters have been changed + public boolean setSlotCount(int slotCount) { + boolean changed = mLayout.setSlotCount(slotCount); + + // mStartIndex is applied the first time setSlotCount is called. + if (mStartIndex != INDEX_NONE) { + setCenterIndex(mStartIndex); + mStartIndex = INDEX_NONE; + } + updateScrollPosition(WIDE ? mScrollX : mScrollY, true); + return changed; + } + + public int getVisibleStart() { + return mLayout.getVisibleStart(); + } + + public int getVisibleEnd() { + return mLayout.getVisibleEnd(); + } + + public int getScrollX() { + return mScrollX; + } + + public int getScrollY() { + return mScrollY; + } +} diff --git a/src/com/android/gallery3d/ui/StaticBackground.java b/src/com/android/gallery3d/ui/StaticBackground.java new file mode 100644 index 000000000..08c55c378 --- /dev/null +++ b/src/com/android/gallery3d/ui/StaticBackground.java @@ -0,0 +1,62 @@ +/* + * 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.ui; + +import android.content.Context; + +public class StaticBackground extends GLView { + + private Context mContext; + private int mLandscapeResource; + private int mPortraitResource; + + private BasicTexture mBackground; + private boolean mIsLandscape = false; + + public StaticBackground(Context context) { + mContext = context; + } + + @Override + protected void onLayout(boolean changeSize, int l, int t, int r, int b) { + setOrientation(getWidth() >= getHeight()); + } + + private void setOrientation(boolean isLandscape) { + if (mIsLandscape == isLandscape) return; + mIsLandscape = isLandscape; + if (mBackground != null) mBackground.recycle(); + mBackground = new ResourceTexture( + mContext, mIsLandscape ? mLandscapeResource : mPortraitResource); + invalidate(); + } + + public void setImage(int landscapeId, int portraitId) { + mLandscapeResource = landscapeId; + mPortraitResource = portraitId; + if (mBackground != null) mBackground.recycle(); + mBackground = new ResourceTexture( + mContext, mIsLandscape ? landscapeId : portraitId); + invalidate(); + } + + @Override + protected void render(GLCanvas canvas) { + //mBackground.draw(canvas, 0, 0, getWidth(), getHeight()); + canvas.fillRect(0, 0, getWidth(), getHeight(), 0xFF000000); + } +} diff --git a/src/com/android/gallery3d/ui/StringTexture.java b/src/com/android/gallery3d/ui/StringTexture.java new file mode 100644 index 000000000..71ab9b351 --- /dev/null +++ b/src/com/android/gallery3d/ui/StringTexture.java @@ -0,0 +1,92 @@ +/* + * 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.ui; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint.FontMetricsInt; +import android.graphics.Typeface; +import android.text.TextPaint; +import android.text.TextUtils; + +// StringTexture is a texture shows the content of a specified String. +// +// To create a StringTexture, use the newInstance() method and specify +// the String, the font size, and the color. +class StringTexture extends CanvasTexture { + private final String mText; + private final TextPaint mPaint; + private final FontMetricsInt mMetrics; + + private StringTexture(String text, TextPaint paint, + FontMetricsInt metrics, int width, int height) { + super(width, height); + mText = text; + mPaint = paint; + mMetrics = metrics; + } + + public static TextPaint getDefaultPaint(float textSize, int color) { + TextPaint paint = new TextPaint(); + paint.setTextSize(textSize); + paint.setAntiAlias(true); + paint.setColor(color); + paint.setShadowLayer(2f, 0f, 0f, Color.BLACK); + return paint; + } + + public static StringTexture newInstance( + String text, float textSize, int color) { + return newInstance(text, getDefaultPaint(textSize, color)); + } + + public static StringTexture newInstance( + String text, String postfix, float textSize, int color, + float lengthLimit, boolean isBold) { + TextPaint paint = getDefaultPaint(textSize, color); + if (isBold) { + paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); + } + if (postfix != null) { + lengthLimit = Math.max(0, + lengthLimit - paint.measureText(postfix)); + text = TextUtils.ellipsize(text, paint, lengthLimit, + TextUtils.TruncateAt.END).toString() + postfix; + } else { + text = TextUtils.ellipsize( + text, paint, lengthLimit, TextUtils.TruncateAt.END).toString(); + } + return newInstance(text, paint); + } + + private static StringTexture newInstance(String text, TextPaint paint) { + FontMetricsInt metrics = paint.getFontMetricsInt(); + int width = (int) Math.ceil(paint.measureText(text)); + int height = metrics.bottom - metrics.top; + // The texture size needs to be at least 1x1. + if (width <= 0) width = 1; + if (height <= 0) height = 1; + return new StringTexture(text, paint, metrics, width, height); + } + + @Override + protected void onDraw(Canvas canvas, Bitmap backing) { + canvas.translate(0, -mMetrics.ascent); + canvas.drawText(mText, 0, 0, mPaint); + } +} diff --git a/src/com/android/gallery3d/ui/StripDrawer.java b/src/com/android/gallery3d/ui/StripDrawer.java new file mode 100644 index 000000000..09106128f --- /dev/null +++ b/src/com/android/gallery3d/ui/StripDrawer.java @@ -0,0 +1,57 @@ +/* + * 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.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.Path; + +import android.content.Context; +import android.graphics.Rect; + +public class StripDrawer extends SelectionDrawer { + private NinePatchTexture mFocusBox; + private Rect mFocusBoxPadding; + + public StripDrawer(Context context) { + mFocusBox = new NinePatchTexture(context, R.drawable.focus_box); + mFocusBoxPadding = mFocusBox.getPaddings(); + } + + @Override + public void prepareDrawing() { + } + + @Override + public void draw(GLCanvas canvas, Texture content, int width, int height, + int rotation, Path path, int topIndex, int dataSourceType, + int mediaType, boolean wantCache, boolean isCaching) { + + int x = -width / 2; + int y = -height / 2; + + drawWithRotation(canvas, content, x, y, width, height, rotation); + } + + @Override + public void drawFocus(GLCanvas canvas, int width, int height) { + int x = -width / 2; + int y = -height / 2; + Rect p = mFocusBoxPadding; + mFocusBox.draw(canvas, x - p.left, y - p.top, + width + p.left + p.right, height + p.top + p.bottom); + } +} diff --git a/src/com/android/gallery3d/ui/SynchronizedHandler.java b/src/com/android/gallery3d/ui/SynchronizedHandler.java new file mode 100644 index 000000000..bd494a331 --- /dev/null +++ b/src/com/android/gallery3d/ui/SynchronizedHandler.java @@ -0,0 +1,41 @@ +/* + * 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.ui; + +import com.android.gallery3d.common.Utils; + +import android.os.Handler; +import android.os.Message; + +public class SynchronizedHandler extends Handler { + + private final GLRoot mRoot; + + public SynchronizedHandler(GLRoot root) { + mRoot = Utils.checkNotNull(root); + } + + @Override + public void dispatchMessage(Message message) { + mRoot.lockRenderThread(); + try { + super.dispatchMessage(message); + } finally { + mRoot.unlockRenderThread(); + } + } +} diff --git a/src/com/android/gallery3d/ui/TextButton.java b/src/com/android/gallery3d/ui/TextButton.java new file mode 100644 index 000000000..c6b85bf55 --- /dev/null +++ b/src/com/android/gallery3d/ui/TextButton.java @@ -0,0 +1,91 @@ +/* + * 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.ui; + +import static com.android.gallery3d.ui.TextButtonConfig.*; + +import android.content.Context; +import android.graphics.Rect; +import android.view.MotionEvent; + +public class TextButton extends Label { + private static final String TAG = "TextButton"; + private boolean mPressed; + private Texture mPressedBackground; + private Texture mNormalBackground; + private OnClickedListener mOnClickListener; + + public interface OnClickedListener { + public void onClicked(GLView source); + } + + public TextButton(Context context, int label) { + super(context, label); + setPaddings(HORIZONTAL_PADDINGS, VERTICAL_PADDINGS, + HORIZONTAL_PADDINGS, VERTICAL_PADDINGS); + } + + public void setOnClickListener(OnClickedListener listener) { + mOnClickListener = listener; + } + + public void setPressedBackground(Texture texture) { + mPressedBackground = texture; + } + + public void setNormalBackground(Texture texture) { + mNormalBackground = texture; + } + + @SuppressWarnings("fallthrough") + @Override + protected boolean onTouch(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mPressed = true; + invalidate(); + break; + case MotionEvent.ACTION_UP: + if (mOnClickListener != null) { + mOnClickListener.onClicked(this); + } + // fall-through + case MotionEvent.ACTION_CANCEL: + mPressed = false; + invalidate(); + break; + } + return true; + } + + @Override + protected void render(GLCanvas canvas) { + Texture bg = mPressed ? mPressedBackground : mNormalBackground; + if (bg != null) { + int width = getWidth(); + int height = getHeight(); + if (bg instanceof NinePatchTexture) { + Rect p = ((NinePatchTexture) bg).getPaddings(); + bg.draw(canvas, -p.left, -p.top, + width + p.left + p.right, height + p.top + p.bottom); + } else { + bg.draw(canvas, 0, 0, width, height); + } + } + super.render(canvas); + } +} diff --git a/src/com/android/gallery3d/ui/Texture.java b/src/com/android/gallery3d/ui/Texture.java new file mode 100644 index 000000000..feb7b0ab7 --- /dev/null +++ b/src/com/android/gallery3d/ui/Texture.java @@ -0,0 +1,44 @@ +/* + * 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.ui; + +// Texture is a rectangular image which can be drawn on GLCanvas. +// The isOpaque() function gives a hint about whether the texture is opaque, +// so the drawing can be done faster. +// +// This is the current texture hierarchy: +// +// Texture +// -- ColorTexture +// -- BasicTexture +// -- RawTexture +// -- UploadedTexture +// -- BitmapTexture +// -- Tile +// -- ResourceTexture +// -- NinePatchTexture +// -- CanvasTexture +// -- DrawableTexture +// -- StringTexture +// +public interface Texture { + public int getWidth(); + public int getHeight(); + public void draw(GLCanvas canvas, int x, int y); + public void draw(GLCanvas canvas, int x, int y, int w, int h); + public boolean isOpaque(); +} diff --git a/src/com/android/gallery3d/ui/TileImageView.java b/src/com/android/gallery3d/ui/TileImageView.java new file mode 100644 index 000000000..cf0685191 --- /dev/null +++ b/src/com/android/gallery3d/ui/TileImageView.java @@ -0,0 +1,693 @@ +/* + * 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.ui; + +import com.android.gallery3d.app.GalleryContext; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.ThreadPool; +import com.android.gallery3d.util.ThreadPool.CancelListener; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.RectF; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +public class TileImageView extends GLView { + public static final int SIZE_UNKNOWN = -1; + + @SuppressWarnings("unused") + private static final String TAG = "TileImageView"; + + // TILE_SIZE must be 2^N - 2. We put one pixel border in each side of the + // texture to avoid seams between tiles. + private static final int TILE_SIZE = 254; + private static final int TILE_BORDER = 1; + private static final int UPLOAD_LIMIT = 1; + + /* + * This is the tile state in the CPU side. + * Life of a Tile: + * ACTIVATED (initial state) + * --> IN_QUEUE - by queueForDecode() + * --> RECYCLED - by recycleTile() + * IN_QUEUE --> DECODING - by decodeTile() + * --> RECYCLED - by recycleTile) + * DECODING --> RECYCLING - by recycleTile() + * --> DECODED - by decodeTile() + * RECYCLING --> RECYCLED - by decodeTile() + * DECODED --> ACTIVATED - (after the decoded bitmap is uploaded) + * DECODED --> RECYCLED - by recycleTile() + * RECYCLED --> ACTIVATED - by obtainTile() + */ + private static final int STATE_ACTIVATED = 0x01; + private static final int STATE_IN_QUEUE = 0x02; + private static final int STATE_DECODING = 0x04; + private static final int STATE_DECODED = 0x08; + private static final int STATE_RECYCLING = 0x10; + private static final int STATE_RECYCLED = 0x20; + + private Model mModel; + protected BitmapTexture mBackupImage; + protected int mLevelCount; // cache the value of mScaledBitmaps.length + + // The mLevel variable indicates which level of bitmap we should use. + // Level 0 means the original full-sized bitmap, and a larger value means + // a smaller scaled bitmap (The width and height of each scaled bitmap is + // half size of the previous one). If the value is in [0, mLevelCount), we + // use the bitmap in mScaledBitmaps[mLevel] for display, otherwise the value + // is mLevelCount, and that means we use mBackupTexture for display. + private int mLevel = 0; + + // The offsets of the (left, top) of the upper-left tile to the (left, top) + // of the view. + private int mOffsetX; + private int mOffsetY; + + private int mUploadQuota; + private boolean mRenderComplete; + + private final RectF mSourceRect = new RectF(); + private final RectF mTargetRect = new RectF(); + + private final HashMap<Long, Tile> mActiveTiles = new HashMap<Long, Tile>(); + + // The following three queue is guarded by TileImageView.this + private TileQueue mRecycledQueue = new TileQueue(); + private TileQueue mUploadQueue = new TileQueue(); + private TileQueue mDecodeQueue = new TileQueue(); + + // The width and height of the full-sized bitmap + protected int mImageWidth = SIZE_UNKNOWN; + protected int mImageHeight = SIZE_UNKNOWN; + + protected int mCenterX; + protected int mCenterY; + protected float mScale; + protected int mRotation; + + // Temp variables to avoid memory allocation + private final Rect mTileRange = new Rect(); + private final Rect mActiveRange[] = {new Rect(), new Rect()}; + + private final TileUploader mTileUploader = new TileUploader(); + private boolean mIsTextureFreed; + private Future<Void> mTileDecoder; + private ThreadPool mThreadPool; + private boolean mBackgroundTileUploaded; + + public static interface Model { + public int getLevelCount(); + public Bitmap getBackupImage(); + public int getImageWidth(); + public int getImageHeight(); + + // The method would be called in another thread + public Bitmap getTile(int level, int x, int y, int tileSize); + public boolean isFailedToLoad(); + } + + public TileImageView(GalleryContext context) { + mThreadPool = context.getThreadPool(); + mTileDecoder = mThreadPool.submit(new TileDecoder()); + } + + public void setModel(Model model) { + mModel = model; + if (model != null) notifyModelInvalidated(); + } + + private void updateBackupTexture(Bitmap backup) { + if (backup == null) { + if (mBackupImage != null) mBackupImage.recycle(); + mBackupImage = null; + } else { + if (mBackupImage != null) { + if (mBackupImage.getBitmap() != backup) { + mBackupImage.recycle(); + mBackupImage = new BitmapTexture(backup); + } + } else { + mBackupImage = new BitmapTexture(backup); + } + } + } + + public void notifyModelInvalidated() { + invalidateTiles(); + if (mModel == null) { + mBackupImage = null; + mImageWidth = 0; + mImageHeight = 0; + mLevelCount = 0; + } else { + updateBackupTexture(mModel.getBackupImage()); + mImageWidth = mModel.getImageWidth(); + mImageHeight = mModel.getImageHeight(); + mLevelCount = mModel.getLevelCount(); + } + layoutTiles(mCenterX, mCenterY, mScale, mRotation); + invalidate(); + } + + @Override + protected void onLayout( + boolean changeSize, int left, int top, int right, int bottom) { + super.onLayout(changeSize, left, top, right, bottom); + if (changeSize) layoutTiles(mCenterX, mCenterY, mScale, mRotation); + } + + // Prepare the tiles we want to use for display. + // + // 1. Decide the tile level we want to use for display. + // 2. Decide the tile levels we want to keep as texture (in addition to + // the one we use for display). + // 3. Recycle unused tiles. + // 4. Activate the tiles we want. + private void layoutTiles(int centerX, int centerY, float scale, int rotation) { + // The width and height of this view. + int width = getWidth(); + int height = getHeight(); + + // The tile levels we want to keep as texture is in the range + // [fromLevel, endLevel). + int fromLevel; + int endLevel; + + // We want to use a texture larger than or equal to the display size. + mLevel = Utils.clamp(Utils.floorLog2(1f / scale), 0, mLevelCount); + + // We want to keep one more tile level as texture in addition to what + // we use for display. So it can be faster when the scale moves to the + // next level. We choose a level closer to the current scale. + if (mLevel != mLevelCount) { + Rect range = mTileRange; + getRange(range, centerX, centerY, mLevel, scale, rotation); + mOffsetX = Math.round(width / 2f + (range.left - centerX) * scale); + mOffsetY = Math.round(height / 2f + (range.top - centerY) * scale); + fromLevel = scale * (1 << mLevel) > 0.75f ? mLevel - 1 : mLevel; + } else { + // Activate the tiles of the smallest two levels. + fromLevel = mLevel - 2; + mOffsetX = Math.round(width / 2f - centerX * scale); + mOffsetY = Math.round(height / 2f - centerY * scale); + } + + fromLevel = Math.max(0, Math.min(fromLevel, mLevelCount - 2)); + endLevel = Math.min(fromLevel + 2, mLevelCount); + + Rect range[] = mActiveRange; + for (int i = fromLevel; i < endLevel; ++i) { + getRange(range[i - fromLevel], centerX, centerY, i, rotation); + } + + // If rotation is transient, don't update the tile. + if (rotation % 90 != 0) return; + + synchronized (this) { + mDecodeQueue.clean(); + mUploadQueue.clean(); + mBackgroundTileUploaded = false; + } + + // Recycle unused tiles: if the level of the active tile is outside the + // range [fromLevel, endLevel) or not in the visible range. + Iterator<Map.Entry<Long, Tile>> + iter = mActiveTiles.entrySet().iterator(); + while (iter.hasNext()) { + Tile tile = iter.next().getValue(); + int level = tile.mTileLevel; + if (level < fromLevel || level >= endLevel + || !range[level - fromLevel].contains(tile.mX, tile.mY)) { + iter.remove(); + recycleTile(tile); + } + } + + for (int i = fromLevel; i < endLevel; ++i) { + int size = TILE_SIZE << i; + Rect r = range[i - fromLevel]; + for (int y = r.top, bottom = r.bottom; y < bottom; y += size) { + for (int x = r.left, right = r.right; x < right; x += size) { + activateTile(x, y, i); + } + } + } + invalidate(); + } + + protected synchronized void invalidateTiles() { + mDecodeQueue.clean(); + mUploadQueue.clean(); + // TODO disable decoder + for (Tile tile : mActiveTiles.values()) { + recycleTile(tile); + } + mActiveTiles.clear(); + } + + private void getRange(Rect out, int cX, int cY, int level, int rotation) { + getRange(out, cX, cY, level, 1f / (1 << (level + 1)), rotation); + } + + // If the bitmap is scaled by the given factor "scale", return the + // rectangle containing visible range. The left-top coordinate returned is + // aligned to the tile boundary. + // + // (cX, cY) is the point on the original bitmap which will be put in the + // center of the ImageViewer. + private void getRange(Rect out, + int cX, int cY, int level, float scale, int rotation) { + + double radians = Math.toRadians(-rotation); + double w = getWidth(); + double h = getHeight(); + + double cos = Math.cos(radians); + double sin = Math.sin(radians); + int width = (int) Math.ceil(Math.max( + Math.abs(cos * w - sin * h), Math.abs(cos * w + sin * h))); + int height = (int) Math.ceil(Math.max( + Math.abs(sin * w + cos * h), Math.abs(sin * w - cos * h))); + + int left = (int) Math.floor(cX - width / (2f * scale)); + int top = (int) Math.floor(cY - height / (2f * scale)); + int right = (int) Math.ceil(left + width / scale); + int bottom = (int) Math.ceil(top + height / scale); + + // align the rectangle to tile boundary + int size = TILE_SIZE << level; + left = Math.max(0, size * (left / size)); + top = Math.max(0, size * (top / size)); + right = Math.min(mImageWidth, right); + bottom = Math.min(mImageHeight, bottom); + + out.set(left, top, right, bottom); + } + + public boolean setPosition(int centerX, int centerY, float scale, int rotation) { + if (mCenterX == centerX + && mCenterY == centerY && mScale == scale) return false; + mCenterX = centerX; + mCenterY = centerY; + mScale = scale; + mRotation = rotation; + layoutTiles(centerX, centerY, scale, rotation); + invalidate(); + return true; + } + + public void freeTextures() { + mIsTextureFreed = true; + + if (mTileDecoder != null) { + mTileDecoder.cancel(); + mTileDecoder.get(); + mTileDecoder = null; + } + + for (Tile texture : mActiveTiles.values()) { + texture.recycle(); + } + mTileRange.set(0, 0, 0, 0); + mActiveTiles.clear(); + + synchronized (this) { + mUploadQueue.clean(); + mDecodeQueue.clean(); + Tile tile = mRecycledQueue.pop(); + while (tile != null) { + tile.recycle(); + tile = mRecycledQueue.pop(); + } + } + updateBackupTexture(null); + } + + public void prepareTextures() { + if (mTileDecoder == null) { + mTileDecoder = mThreadPool.submit(new TileDecoder()); + } + if (mIsTextureFreed) { + layoutTiles(mCenterX, mCenterY, mScale, mRotation); + mIsTextureFreed = false; + updateBackupTexture(mModel.getBackupImage()); + } + } + + @Override + protected void render(GLCanvas canvas) { + mUploadQuota = UPLOAD_LIMIT; + mRenderComplete = true; + + int level = mLevel; + int rotation = mRotation; + + if (rotation != 0) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + int centerX = getWidth() / 2, centerY = getHeight() / 2; + canvas.translate(centerX, centerY, 0); + canvas.rotate(rotation, 0, 0, 1); + canvas.translate(-centerX, -centerY, 0); + } + try { + if (level != mLevelCount) { + int size = (TILE_SIZE << level); + float length = size * mScale; + Rect r = mTileRange; + + for (int ty = r.top, i = 0; ty < r.bottom; ty += size, i++) { + float y = mOffsetY + i * length; + for (int tx = r.left, j = 0; tx < r.right; tx += size, j++) { + float x = mOffsetX + j * length; + drawTile(canvas, tx, ty, level, x, y, length); + } + } + } else if (mBackupImage != null) { + mBackupImage.draw(canvas, mOffsetX, mOffsetY, + Math.round(mImageWidth * mScale), + Math.round(mImageHeight * mScale)); + } + } finally { + if (rotation != 0) canvas.restore(); + } + + if (mRenderComplete) { + if (!mBackgroundTileUploaded) uploadBackgroundTiles(canvas); + } else { + invalidate(); + } + } + + private void uploadBackgroundTiles(GLCanvas canvas) { + mBackgroundTileUploaded = true; + for (Tile tile : mActiveTiles.values()) { + if (!tile.isContentValid(canvas)) queueForDecode(tile); + } + } + + void queueForUpload(Tile tile) { + synchronized (this) { + mUploadQueue.push(tile); + } + if (mTileUploader.mActive.compareAndSet(false, true)) { + getGLRoot().addOnGLIdleListener(mTileUploader); + } + } + + synchronized void queueForDecode(Tile tile) { + if (tile.mTileState == STATE_ACTIVATED) { + tile.mTileState = STATE_IN_QUEUE; + if (mDecodeQueue.push(tile)) notifyAll(); + } + } + + boolean decodeTile(Tile tile) { + synchronized (this) { + if (tile.mTileState != STATE_IN_QUEUE) return false; + tile.mTileState = STATE_DECODING; + } + boolean decodeComplete = tile.decode(); + synchronized (this) { + if (tile.mTileState == STATE_RECYCLING) { + tile.mTileState = STATE_RECYCLED; + tile.mDecodedTile = null; + mRecycledQueue.push(tile); + return false; + } + tile.mTileState = STATE_DECODED; + return decodeComplete; + } + } + + private synchronized Tile obtainTile(int x, int y, int level) { + Tile tile = mRecycledQueue.pop(); + if (tile != null) { + tile.mTileState = STATE_ACTIVATED; + tile.update(x, y, level); + return tile; + } + return new Tile(x, y, level); + } + + synchronized void recycleTile(Tile tile) { + if (tile.mTileState == STATE_DECODING) { + tile.mTileState = STATE_RECYCLING; + return; + } + tile.mTileState = STATE_RECYCLED; + tile.mDecodedTile = null; + mRecycledQueue.push(tile); + } + + private void activateTile(int x, int y, int level) { + Long key = makeTileKey(x, y, level); + Tile tile = mActiveTiles.get(key); + if (tile != null) { + if (tile.mTileState == STATE_IN_QUEUE) { + tile.mTileState = STATE_ACTIVATED; + } + return; + } + tile = obtainTile(x, y, level); + mActiveTiles.put(key, tile); + } + + private Tile getTile(int x, int y, int level) { + return mActiveTiles.get(makeTileKey(x, y, level)); + } + + private static Long makeTileKey(int x, int y, int level) { + long result = x; + result = (result << 16) | y; + result = (result << 16) | level; + return Long.valueOf(result); + } + + private class TileUploader implements GLRoot.OnGLIdleListener { + AtomicBoolean mActive = new AtomicBoolean(false); + + @Override + public boolean onGLIdle(GLRoot root, GLCanvas canvas) { + int quota = UPLOAD_LIMIT; + Tile tile; + while (true) { + synchronized (TileImageView.this) { + tile = mUploadQueue.pop(); + } + if (tile == null || quota <= 0) break; + if (!tile.isContentValid(canvas)) { + Utils.assertTrue(tile.mTileState == STATE_DECODED); + tile.updateContent(canvas); + --quota; + } + } + mActive.set(tile != null); + return tile != null; + } + } + + // Draw the tile to a square at canvas that locates at (x, y) and + // has a side length of length. + public void drawTile(GLCanvas canvas, + int tx, int ty, int level, float x, float y, float length) { + RectF source = mSourceRect; + RectF target = mTargetRect; + target.set(x, y, x + length, y + length); + source.set(0, 0, TILE_SIZE, TILE_SIZE); + + Tile tile = getTile(tx, ty, level); + if (tile != null) { + if (!tile.isContentValid(canvas)) { + if (tile.mTileState == STATE_DECODED) { + if (mUploadQuota > 0) { + --mUploadQuota; + tile.updateContent(canvas); + } else { + mRenderComplete = false; + } + } else { + mRenderComplete = false; + queueForDecode(tile); + } + } + if (drawTile(tile, canvas, source, target)) return; + } + if (mBackupImage != null) { + BasicTexture backup = mBackupImage; + int size = TILE_SIZE << level; + float scaleX = (float) backup.getWidth() / mImageWidth; + float scaleY = (float) backup.getHeight() / mImageHeight; + source.set(tx * scaleX, ty * scaleY, (tx + size) * scaleX, + (ty + size) * scaleY); + canvas.drawTexture(backup, source, target); + } + } + + // TODO: avoid drawing the unused part of the textures. + static boolean drawTile( + Tile tile, GLCanvas canvas, RectF source, RectF target) { + while (true) { + if (tile.isContentValid(canvas)) { + // offset source rectangle for the texture border. + source.offset(TILE_BORDER, TILE_BORDER); + canvas.drawTexture(tile, source, target); + return true; + } + + // Parent can be divided to four quads and tile is one of the four. + Tile parent = tile.getParentTile(); + if (parent == null) return false; + if (tile.mX == parent.mX) { + source.left /= 2f; + source.right /= 2f; + } else { + source.left = (TILE_SIZE + source.left) / 2f; + source.right = (TILE_SIZE + source.right) / 2f; + } + if (tile.mY == parent.mY) { + source.top /= 2f; + source.bottom /= 2f; + } else { + source.top = (TILE_SIZE + source.top) / 2f; + source.bottom = (TILE_SIZE + source.bottom) / 2f; + } + tile = parent; + } + } + + private class Tile extends UploadedTexture { + int mX; + int mY; + int mTileLevel; + Tile mNext; + Bitmap mDecodedTile; + volatile int mTileState = STATE_ACTIVATED; + + public Tile(int x, int y, int level) { + mX = x; + mY = y; + mTileLevel = level; + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + bitmap.recycle(); + } + + boolean decode() { + // Get a tile from the original image. The tile is down-scaled + // by (1 << mTilelevel) from a region in the original image. + int tileLength = (TILE_SIZE + 2 * TILE_BORDER); + int borderLength = TILE_BORDER << mTileLevel; + try { + mDecodedTile = mModel.getTile( + mTileLevel, mX - borderLength, mY - borderLength, tileLength); + return mDecodedTile != null; + } catch (Throwable t) { + Log.w(TAG, "fail to decode tile", t); + return false; + } + } + + @Override + protected Bitmap onGetBitmap() { + Utils.assertTrue(mTileState == STATE_DECODED); + Bitmap bitmap = mDecodedTile; + mDecodedTile = null; + mTileState = STATE_ACTIVATED; + return bitmap; + } + + public void update(int x, int y, int level) { + mX = x; + mY = y; + mTileLevel = level; + invalidateContent(); + } + + public Tile getParentTile() { + if (mTileLevel + 1 == mLevelCount) return null; + int size = TILE_SIZE << (mTileLevel + 1); + int x = size * (mX / size); + int y = size * (mY / size); + return getTile(x, y, mTileLevel + 1); + } + + @Override + public String toString() { + return String.format("tile(%s, %s, %s / %s)", + mX / TILE_SIZE, mY / TILE_SIZE, mLevel, mLevelCount); + } + } + + private static class TileQueue { + private Tile mHead; + + public Tile pop() { + Tile tile = mHead; + if (tile != null) mHead = tile.mNext; + return tile; + } + + public boolean push(Tile tile) { + boolean wasEmpty = mHead == null; + tile.mNext = mHead; + mHead = tile; + return wasEmpty; + } + + public void clean() { + mHead = null; + } + } + + private class TileDecoder implements ThreadPool.Job<Void> { + + private CancelListener mNotifier = new CancelListener() { + @Override + public void onCancel() { + synchronized (TileImageView.this) { + TileImageView.this.notifyAll(); + } + } + }; + + @Override + public Void run(JobContext jc) { + jc.setMode(ThreadPool.MODE_NONE); + jc.setCancelListener(mNotifier); + while (!jc.isCancelled()) { + Tile tile = null; + synchronized(TileImageView.this) { + tile = mDecodeQueue.pop(); + if (tile == null && !jc.isCancelled()) { + Utils.waitWithoutInterrupt(TileImageView.this); + } + } + if (tile == null) continue; + if (decodeTile(tile)) queueForUpload(tile); + } + return null; + } + } +} diff --git a/src/com/android/gallery3d/ui/TileImageViewAdapter.java b/src/com/android/gallery3d/ui/TileImageViewAdapter.java new file mode 100644 index 000000000..65dea0eac --- /dev/null +++ b/src/com/android/gallery3d/ui/TileImageViewAdapter.java @@ -0,0 +1,144 @@ +/* + * 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.ui; + +import com.android.gallery3d.common.Utils; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Canvas; +import android.graphics.Rect; + +public class TileImageViewAdapter implements TileImageView.Model { + protected BitmapRegionDecoder mRegionDecoder; + protected int mImageWidth; + protected int mImageHeight; + protected Bitmap mBackupImage; + protected int mLevelCount; + protected boolean mFailedToLoad; + + private final Rect mIntersectRect = new Rect(); + private final Rect mRegionRect = new Rect(); + + public TileImageViewAdapter() { + } + + public TileImageViewAdapter(Bitmap backup, BitmapRegionDecoder regionDecoder) { + mBackupImage = Utils.checkNotNull(backup); + mRegionDecoder = regionDecoder; + mImageWidth = regionDecoder.getWidth(); + mImageHeight = regionDecoder.getHeight(); + mLevelCount = calculateLevelCount(); + } + + public synchronized void clear() { + mBackupImage = null; + mImageWidth = 0; + mImageHeight = 0; + mLevelCount = 0; + mRegionDecoder = null; + mFailedToLoad = false; + } + + public synchronized void setBackupImage(Bitmap backup, int width, int height) { + mBackupImage = Utils.checkNotNull(backup); + mImageWidth = width; + mImageHeight = height; + mRegionDecoder = null; + mLevelCount = 0; + mFailedToLoad = false; + } + + public synchronized void setRegionDecoder(BitmapRegionDecoder decoder) { + mRegionDecoder = Utils.checkNotNull(decoder); + mImageWidth = decoder.getWidth(); + mImageHeight = decoder.getHeight(); + mLevelCount = calculateLevelCount(); + mFailedToLoad = false; + } + + private int calculateLevelCount() { + return Math.max(0, Utils.ceilLog2( + (float) mImageWidth / mBackupImage.getWidth())); + } + + @Override + public synchronized Bitmap getTile(int level, int x, int y, int length) { + Rect region = mRegionRect; + Rect intersectRect = mIntersectRect; + region.set(x, y, x + (length << level), y + (length << level)); + intersectRect.set(0, 0, mImageWidth, mImageHeight); + + // Get the intersected rect of the requested region and the image. + Utils.assertTrue(intersectRect.intersect(region)); + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Config.ARGB_8888; + options.inPreferQualityOverSpeed = true; + options.inSampleSize = (1 << level); + + Bitmap bitmap; + + // In CropImage, we may call the decodeRegion() concurrently. + synchronized (mRegionDecoder) { + bitmap = mRegionDecoder.decodeRegion(intersectRect, options); + } + + // The returned region may not match with the targetLength. + // If so, we fill black pixels on it. + if (intersectRect.equals(region)) return bitmap; + + Bitmap tile = Bitmap.createBitmap(length, length, Config.ARGB_8888); + Canvas canvas = new Canvas(tile); + canvas.drawBitmap(bitmap, + (intersectRect.left - region.left) >> level, + (intersectRect.top - region.top) >> level, null); + bitmap.recycle(); + return tile; + } + + @Override + public Bitmap getBackupImage() { + return mBackupImage; + } + + @Override + public int getImageHeight() { + return mImageHeight; + } + + @Override + public int getImageWidth() { + return mImageWidth; + } + + @Override + public int getLevelCount() { + return mLevelCount; + } + + public void setFailedToLoad() { + mFailedToLoad = true; + } + + @Override + public boolean isFailedToLoad() { + return mFailedToLoad; + } +} diff --git a/src/com/android/gallery3d/ui/UploadedTexture.java b/src/com/android/gallery3d/ui/UploadedTexture.java new file mode 100644 index 000000000..b063824d2 --- /dev/null +++ b/src/com/android/gallery3d/ui/UploadedTexture.java @@ -0,0 +1,285 @@ +/* + * 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.ui; + +import com.android.gallery3d.common.Utils; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.opengl.GLUtils; + +import java.util.HashMap; +import javax.microedition.khronos.opengles.GL11; +import javax.microedition.khronos.opengles.GL11Ext; + +// UploadedTextures use a Bitmap for the content of the texture. +// +// Subclasses should implement onGetBitmap() to provide the Bitmap and +// implement onFreeBitmap(mBitmap) which will be called when the Bitmap +// is not needed anymore. +// +// isContentValid() is meaningful only when the isLoaded() returns true. +// It means whether the content needs to be updated. +// +// The user of this class should call recycle() when the texture is not +// needed anymore. +// +// By default an UploadedTexture is opaque (so it can be drawn faster without +// blending). The user or subclass can override it using setOpaque(). +abstract class UploadedTexture extends BasicTexture { + + // To prevent keeping allocation the borders, we store those used borders here. + // Since the length will be power of two, it won't use too much memory. + private static HashMap<BorderKey, Bitmap> sBorderLines = + new HashMap<BorderKey, Bitmap>(); + private static BorderKey sBorderKey = new BorderKey(); + + @SuppressWarnings("unused") + private static final String TAG = "Texture"; + private boolean mContentValid = true; + private boolean mOpaque = true; + private boolean mThrottled = false; + private static int sUploadedCount; + private static final int UPLOAD_LIMIT = 100; + + protected Bitmap mBitmap; + + protected UploadedTexture() { + super(null, 0, STATE_UNLOADED); + } + + private static class BorderKey implements Cloneable { + public boolean vertical; + public Config config; + public int length; + + @Override + public int hashCode() { + int x = config.hashCode() ^ length; + return vertical ? x : -x; + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof BorderKey)) return false; + BorderKey o = (BorderKey) object; + return vertical == o.vertical + && config == o.config && length == o.length; + } + + @Override + public BorderKey clone() { + try { + return (BorderKey) super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(e); + } + } + } + + protected void setThrottled(boolean throttled) { + mThrottled = throttled; + } + + private static Bitmap getBorderLine( + boolean vertical, Config config, int length) { + BorderKey key = sBorderKey; + key.vertical = vertical; + key.config = config; + key.length = length; + Bitmap bitmap = sBorderLines.get(key); + if (bitmap == null) { + bitmap = vertical + ? Bitmap.createBitmap(1, length, config) + : Bitmap.createBitmap(length, 1, config); + sBorderLines.put(key.clone(), bitmap); + } + return bitmap; + } + + private Bitmap getBitmap() { + if (mBitmap == null) { + mBitmap = onGetBitmap(); + if (mWidth == UNSPECIFIED) { + setSize(mBitmap.getWidth(), mBitmap.getHeight()); + } else if (mWidth != mBitmap.getWidth() + || mHeight != mBitmap.getHeight()) { + throw new IllegalStateException(String.format( + "cannot change size: this = %s, orig = %sx%s, new = %sx%s", + toString(), mWidth, mHeight, mBitmap.getWidth(), + mBitmap.getHeight())); + } + } + return mBitmap; + } + + private void freeBitmap() { + Utils.assertTrue(mBitmap != null); + onFreeBitmap(mBitmap); + mBitmap = null; + } + + @Override + public int getWidth() { + if (mWidth == UNSPECIFIED) getBitmap(); + return mWidth; + } + + @Override + public int getHeight() { + if (mWidth == UNSPECIFIED) getBitmap(); + return mHeight; + } + + protected abstract Bitmap onGetBitmap(); + + protected abstract void onFreeBitmap(Bitmap bitmap); + + protected void invalidateContent() { + if (mBitmap != null) freeBitmap(); + mContentValid = false; + } + + /** + * Whether the content on GPU is valid. + */ + public boolean isContentValid(GLCanvas canvas) { + return isLoaded(canvas) && mContentValid; + } + + /** + * Updates the content on GPU's memory. + * @param canvas + */ + public void updateContent(GLCanvas canvas) { + if (!isLoaded(canvas)) { + if (mThrottled && ++sUploadedCount > UPLOAD_LIMIT) { + return; + } + uploadToCanvas(canvas); + } else if (!mContentValid) { + Bitmap bitmap = getBitmap(); + int format = GLUtils.getInternalFormat(bitmap); + int type = GLUtils.getType(bitmap); + canvas.getGLInstance().glBindTexture(GL11.GL_TEXTURE_2D, mId); + GLUtils.texSubImage2D( + GL11.GL_TEXTURE_2D, 0, 0, 0, bitmap, format, type); + freeBitmap(); + mContentValid = true; + } + } + + public static void resetUploadLimit() { + sUploadedCount = 0; + } + + public static boolean uploadLimitReached() { + return sUploadedCount > UPLOAD_LIMIT; + } + + static int[] sTextureId = new int[1]; + static float[] sCropRect = new float[4]; + + private void uploadToCanvas(GLCanvas canvas) { + GL11 gl = canvas.getGLInstance(); + + Bitmap bitmap = getBitmap(); + if (bitmap != null) { + try { + // Define a vertically flipped crop rectangle for + // OES_draw_texture. + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + sCropRect[0] = 0; + sCropRect[1] = height; + sCropRect[2] = width; + sCropRect[3] = -height; + + // Upload the bitmap to a new texture. + gl.glGenTextures(1, sTextureId, 0); + gl.glBindTexture(GL11.GL_TEXTURE_2D, sTextureId[0]); + gl.glTexParameterfv(GL11.GL_TEXTURE_2D, + GL11Ext.GL_TEXTURE_CROP_RECT_OES, sCropRect, 0); + gl.glTexParameteri(GL11.GL_TEXTURE_2D, + GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE); + gl.glTexParameteri(GL11.GL_TEXTURE_2D, + GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE); + gl.glTexParameterf(GL11.GL_TEXTURE_2D, + GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + gl.glTexParameterf(GL11.GL_TEXTURE_2D, + GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + + if (width == getTextureWidth() && height == getTextureHeight()) { + GLUtils.texImage2D(GL11.GL_TEXTURE_2D, 0, bitmap, 0); + } else { + int format = GLUtils.getInternalFormat(bitmap); + int type = GLUtils.getType(bitmap); + Config config = bitmap.getConfig(); + + gl.glTexImage2D(GL11.GL_TEXTURE_2D, 0, format, + getTextureWidth(), getTextureHeight(), + 0, format, type, null); + GLUtils.texSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, bitmap, + format, type); + + if (width != getTextureWidth()) { + Bitmap line = getBorderLine(true, config, getTextureHeight()); + GLUtils.texSubImage2D( + GL11.GL_TEXTURE_2D, 0, width, 0, line, format, type); + } + + if (height != getTextureHeight()) { + Bitmap line = getBorderLine(false, config, getTextureWidth()); + GLUtils.texSubImage2D( + GL11.GL_TEXTURE_2D, 0, 0, height, line, format, type); + } + + } + } finally { + freeBitmap(); + } + // Update texture state. + setAssociatedCanvas(canvas); + mId = sTextureId[0]; + mState = UploadedTexture.STATE_LOADED; + mContentValid = true; + } else { + mState = STATE_ERROR; + throw new RuntimeException("Texture load fail, no bitmap"); + } + } + + @Override + protected boolean onBind(GLCanvas canvas) { + updateContent(canvas); + return isContentValid(canvas); + } + + public void setOpaque(boolean isOpaque) { + mOpaque = isOpaque; + } + + public boolean isOpaque() { + return mOpaque; + } + + @Override + public void recycle() { + super.recycle(); + if (mBitmap != null) freeBitmap(); + } +} diff --git a/src/com/android/gallery3d/ui/UserInteractionListener.java b/src/com/android/gallery3d/ui/UserInteractionListener.java new file mode 100644 index 000000000..bc4a71800 --- /dev/null +++ b/src/com/android/gallery3d/ui/UserInteractionListener.java @@ -0,0 +1,26 @@ +/* + * 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.ui; + +public interface UserInteractionListener { + // Called when a user interaction begins (for example, fling). + public void onUserInteractionBegin(); + // Called when the user interaction ends. + public void onUserInteractionEnd(); + // Other one-shot user interactions. + public void onUserInteraction(); +} diff --git a/src/com/android/gallery3d/util/CacheManager.java b/src/com/android/gallery3d/util/CacheManager.java new file mode 100644 index 000000000..fcc444e98 --- /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 com.android.gallery3d.common.BlobCache; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +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/Future.java b/src/com/android/gallery3d/util/Future.java new file mode 100644 index 000000000..580a2a120 --- /dev/null +++ b/src/com/android/gallery3d/util/Future.java @@ -0,0 +1,35 @@ +/* + * 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; + +// This Future differs from the java.util.concurrent.Future in these aspects: +// +// - Once cancel() is called, isCancelled() always returns true. It is a sticky +// flag used to communicate to the implementation. The implmentation may +// ignore that flag. Regardless whether the Future is cancelled, a return +// value will be provided to get(). The implementation may choose to return +// null if it finds the Future is cancelled. +// +// - get() does not throw exceptions. +// +public interface Future<T> { + public void cancel(); + public boolean isCancelled(); + public boolean isDone(); + public T get(); + public void waitDone(); +} diff --git a/src/com/android/gallery3d/util/FutureListener.java b/src/com/android/gallery3d/util/FutureListener.java new file mode 100644 index 000000000..ed1f820c7 --- /dev/null +++ b/src/com/android/gallery3d/util/FutureListener.java @@ -0,0 +1,21 @@ +/* + * 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 interface FutureListener<T> { + public void onFutureDone(Future<T> future); +} diff --git a/src/com/android/gallery3d/util/FutureTask.java b/src/com/android/gallery3d/util/FutureTask.java new file mode 100644 index 000000000..9cfab27cb --- /dev/null +++ b/src/com/android/gallery3d/util/FutureTask.java @@ -0,0 +1,86 @@ +/* + * 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.util.concurrent.Callable; + +// NOTE: If the Callable throws any Throwable, the result value will be null. +public class FutureTask<T> implements Runnable, Future<T> { + private static final String TAG = "FutureTask"; + private Callable<T> mCallable; + private FutureListener<T> mListener; + private volatile boolean mIsCancelled; + private boolean mIsDone; + private T mResult; + + public FutureTask(Callable<T> callable, FutureListener<T> listener) { + mCallable = callable; + mListener = listener; + } + + public FutureTask(Callable<T> callable) { + this(callable, null); + } + + public void cancel() { + mIsCancelled = true; + } + + public synchronized T get() { + while (!mIsDone) { + try { + wait(); + } catch (InterruptedException t) { + // ignore. + } + } + return mResult; + } + + public void waitDone() { + get(); + } + + public synchronized boolean isDone() { + return mIsDone; + } + + public boolean isCancelled() { + return mIsCancelled; + } + + public void run() { + T result = null; + + if (!mIsCancelled) { + try { + result = mCallable.call(); + } catch (Throwable ex) { + Log.w(TAG, "Exception in running a task", ex); + } + } + + synchronized(this) { + mResult = result; + mIsDone = true; + if (mListener != null) { + mListener.onFutureDone(this); + } + notifyAll(); + } + } +} diff --git a/src/com/android/gallery3d/util/GalleryUtils.java b/src/com/android/gallery3d/util/GalleryUtils.java new file mode 100644 index 000000000..2fed46a22 --- /dev/null +++ b/src/com/android/gallery3d/util/GalleryUtils.java @@ -0,0 +1,327 @@ +/* + * 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.R; +import com.android.gallery3d.app.PackagesMonitor; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.util.ThreadPool.CancelListener; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import android.app.Activity; +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.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 java.util.Arrays; +import java.util.List; + +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 MIME_TYPE_IMAGE = "image/*"; + private static final String MIME_TYPE_VIDEO = "video/*"; + private static final String MIME_TYPE_ALL = "*/*"; + + 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 Context sContext; + + + static float sPixelDensity = -1f; + + public static void initialize(Context context) { + sContext = context; + if (sPixelDensity < 0) { + DisplayMetrics metrics = new DisplayMetrics(); + WindowManager wm = (WindowManager) + context.getSystemService(Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getMetrics(metrics); + sPixelDensity = metrics.density; + } + } + + 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() { + 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 isCameraAvailable(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 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 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 = String.format("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 = String.format("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(); + } + + // 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; + } + + public static void setSpinnerVisibility(final Activity activity, + final boolean visible) { + activity.runOnUiThread(new Runnable() { + public void run() { + activity.setProgressBarIndeterminateVisibility(visible); + } + }); + } + + public static int determineTypeBits(Context context, Intent intent) { + int typeBits = 0; + String type = intent.resolveType(context); + if (intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false)) { + if (MIME_TYPE_ALL.equals(type)) { + typeBits = DataManager.INCLUDE_LOCAL_ALL_ONLY; + } else if (MIME_TYPE_IMAGE.equals(type)) { + typeBits = DataManager.INCLUDE_LOCAL_IMAGE_ONLY; + } else if (MIME_TYPE_VIDEO.equals(type)) { + typeBits = DataManager.INCLUDE_LOCAL_VIDEO_ONLY; + } + } else { + if (MIME_TYPE_ALL.equals(type)) { + typeBits = DataManager.INCLUDE_ALL; + } else if (MIME_TYPE_IMAGE.equals(type)) { + typeBits = DataManager.INCLUDE_IMAGE; + } else if (MIME_TYPE_VIDEO.equals(type)) { + typeBits = DataManager.INCLUDE_VIDEO; + } + } + if (typeBits == 0) typeBits = DataManager.INCLUDE_ALL; + + 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 void assertInMainThread() { + if (Thread.currentThread() == sContext.getMainLooper().getThread()) { + throw new AssertionError(); + } + } +} diff --git a/src/com/android/gallery3d/util/IdentityCache.java b/src/com/android/gallery3d/util/IdentityCache.java new file mode 100644 index 000000000..02a46aef7 --- /dev/null +++ b/src/com/android/gallery3d/util/IdentityCache.java @@ -0,0 +1,74 @@ +/* + * 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.HashMap; +import java.util.ArrayList; +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(); + } + + public synchronized void clear() { + mWeakMap.clear(); + mQueue = new ReferenceQueue<V>(); + } + + 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..88657bbd6 --- /dev/null +++ b/src/com/android/gallery3d/util/IntArray.java @@ -0,0 +1,54 @@ +/* + * 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 size() { + return mSize; + } + + 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/LinkedNode.java b/src/com/android/gallery3d/util/LinkedNode.java new file mode 100644 index 000000000..8554acd21 --- /dev/null +++ b/src/com/android/gallery3d/util/LinkedNode.java @@ -0,0 +1,75 @@ +/* + * 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 insertFirst(T node) { + mHead.insert(node); + } + + 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..817ffedcb --- /dev/null +++ b/src/com/android/gallery3d/util/MediaSetUtils.java @@ -0,0 +1,56 @@ +/* + * 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.data.MediaSet; +import com.android.gallery3d.data.MtpContext; +import com.android.gallery3d.data.Path; + +import android.os.Environment; + +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() + "/DCIM/Camera"); + public static final int DOWNLOAD_BUCKET_ID = GalleryUtils.getBucketId( + Environment.getExternalStorageDirectory().toString() + "/download"); + public static final int IMPORTED_BUCKET_ID = GalleryUtils.getBucketId( + Environment.getExternalStorageDirectory().toString() + "/" + + MtpContext.NAME_IMPORTED_FOLDER); + + 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> { + 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/PriorityThreadFactory.java b/src/com/android/gallery3d/util/PriorityThreadFactory.java new file mode 100644 index 000000000..67b215274 --- /dev/null +++ b/src/com/android/gallery3d/util/PriorityThreadFactory.java @@ -0,0 +1,48 @@ +/* + * 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.Process; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A thread factory that creates threads with a given thread priority. + */ +public class PriorityThreadFactory implements ThreadFactory { + + private final int mPriority; + private final AtomicInteger mNumber = new AtomicInteger(); + private final String mName; + + public PriorityThreadFactory(String name, int priority) { + mName = name; + mPriority = priority; + } + + public Thread newThread(Runnable r) { + return new Thread(r, mName + '-' + mNumber.getAndIncrement()) { + @Override + public void run() { + Process.setThreadPriority(mPriority); + super.run(); + } + }; + } + +} diff --git a/src/com/android/gallery3d/util/ReverseGeocoder.java b/src/com/android/gallery3d/util/ReverseGeocoder.java new file mode 100644 index 000000000..d253b4b96 --- /dev/null +++ b/src/com/android/gallery3d/util/ReverseGeocoder.java @@ -0,0 +1,417 @@ +/* + * 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.BlobCache; + +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 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 { + 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/ThreadPool.java b/src/com/android/gallery3d/util/ThreadPool.java new file mode 100644 index 000000000..71bb3c5b7 --- /dev/null +++ b/src/com/android/gallery3d/util/ThreadPool.java @@ -0,0 +1,252 @@ +/* + * 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.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class ThreadPool { + private static final String TAG = "ThreadPool"; + private static final int CORE_POOL_SIZE = 4; + private static final int MAX_POOL_SIZE = 8; + private static final int KEEP_ALIVE_TIME = 10; // 10 seconds + + // Resource type + public static final int MODE_NONE = 0; + public static final int MODE_CPU = 1; + public static final int MODE_NETWORK = 2; + + public static final JobContext JOB_CONTEXT_STUB = new JobContextStub(); + + ResourceCounter mCpuCounter = new ResourceCounter(2); + ResourceCounter mNetworkCounter = new ResourceCounter(2); + + // A Job is like a Callable, but it has an addition JobContext parameter. + public interface Job<T> { + public T run(JobContext jc); + } + + public interface JobContext { + boolean isCancelled(); + void setCancelListener(CancelListener listener); + boolean setMode(int mode); + } + + private static class JobContextStub implements JobContext { + @Override + public boolean isCancelled() { + return false; + } + + @Override + public void setCancelListener(CancelListener listener) { + } + + @Override + public boolean setMode(int mode) { + return true; + } + } + + public interface CancelListener { + public void onCancel(); + } + + private static class ResourceCounter { + public int value; + public ResourceCounter(int v) { + value = v; + } + } + + private final Executor mExecutor; + + public ThreadPool() { + mExecutor = new ThreadPoolExecutor( + CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, + TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), + new PriorityThreadFactory("thread-pool", + android.os.Process.THREAD_PRIORITY_BACKGROUND)); + } + + // Submit a job to the thread pool. The listener will be called when the + // job is finished (or cancelled). + public <T> Future<T> submit(Job<T> job, FutureListener<T> listener) { + Worker<T> w = new Worker<T>(job, listener); + mExecutor.execute(w); + return w; + } + + public <T> Future<T> submit(Job<T> job) { + return submit(job, null); + } + + private class Worker<T> implements Runnable, Future<T>, JobContext { + private static final String TAG = "Worker"; + private Job<T> mJob; + private FutureListener<T> mListener; + private CancelListener mCancelListener; + private ResourceCounter mWaitOnResource; + private volatile boolean mIsCancelled; + private boolean mIsDone; + private T mResult; + private int mMode; + + public Worker(Job<T> job, FutureListener<T> listener) { + mJob = job; + mListener = listener; + } + + // This is called by a thread in the thread pool. + public void run() { + T result = null; + + // A job is in CPU mode by default. setMode returns false + // if the job is cancelled. + if (setMode(MODE_CPU)) { + try { + result = mJob.run(this); + } catch (Throwable ex) { + Log.w(TAG, "Exception in running a job", ex); + } + } + + synchronized(this) { + setMode(MODE_NONE); + mResult = result; + mIsDone = true; + notifyAll(); + } + if (mListener != null) mListener.onFutureDone(this); + } + + // Below are the methods for Future. + public synchronized void cancel() { + if (mIsCancelled) return; + mIsCancelled = true; + if (mWaitOnResource != null) { + synchronized (mWaitOnResource) { + mWaitOnResource.notifyAll(); + } + } + if (mCancelListener != null) { + mCancelListener.onCancel(); + } + } + + public boolean isCancelled() { + return mIsCancelled; + } + + public synchronized boolean isDone() { + return mIsDone; + } + + public synchronized T get() { + while (!mIsDone) { + try { + wait(); + } catch (Exception ex) { + Log.w(TAG, "ingore exception", ex); + // ignore. + } + } + return mResult; + } + + public void waitDone() { + get(); + } + + // Below are the methods for JobContext (only called from the + // thread running the job) + public synchronized void setCancelListener(CancelListener listener) { + mCancelListener = listener; + if (mIsCancelled && mCancelListener != null) { + mCancelListener.onCancel(); + } + } + + public boolean setMode(int mode) { + // Release old resource + ResourceCounter rc = modeToCounter(mMode); + if (rc != null) releaseResource(rc); + mMode = MODE_NONE; + + // Acquire new resource + rc = modeToCounter(mode); + if (rc != null) { + if (!acquireResource(rc)) { + return false; + } + mMode = mode; + } + + return true; + } + + private ResourceCounter modeToCounter(int mode) { + if (mode == MODE_CPU) { + return mCpuCounter; + } else if (mode == MODE_NETWORK) { + return mNetworkCounter; + } else { + return null; + } + } + + private boolean acquireResource(ResourceCounter counter) { + while (true) { + synchronized (this) { + if (mIsCancelled) { + mWaitOnResource = null; + return false; + } + mWaitOnResource = counter; + } + + synchronized (counter) { + if (counter.value > 0) { + counter.value--; + break; + } else { + try { + counter.wait(); + } catch (InterruptedException ex) { + // ignore. + } + } + } + } + + synchronized (this) { + mWaitOnResource = null; + } + + return true; + } + + private void releaseResource(ResourceCounter counter) { + synchronized (counter) { + counter.value++; + counter.notifyAll(); + } + } + } +} diff --git a/src/com/android/gallery3d/util/UpdateHelper.java b/src/com/android/gallery3d/util/UpdateHelper.java new file mode 100644 index 000000000..9fdade683 --- /dev/null +++ b/src/com/android/gallery3d/util/UpdateHelper.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; + +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 double update(float original, float 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; + } +} diff --git a/src/com/android/gallery3d/widget/LocalPhotoSource.java b/src/com/android/gallery3d/widget/LocalPhotoSource.java new file mode 100644 index 000000000..de16a7129 --- /dev/null +++ b/src/com/android/gallery3d/widget/LocalPhotoSource.java @@ -0,0 +1,202 @@ +/* + * 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.widget; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.util.GalleryUtils; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Environment; +import android.os.Handler; +import android.provider.MediaStore.Images.Media; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Random; + +public class LocalPhotoSource implements WidgetSource { + + private static final String TAG = "LocalPhotoSource"; + + private static final int MAX_PHOTO_COUNT = 128; + + /* Static fields used to query for the correct set of images */ + private static final Uri CONTENT_URI = Media.EXTERNAL_CONTENT_URI; + private static final String DATE_TAKEN = Media.DATE_TAKEN; + private static final String[] PROJECTION = {Media._ID}; + private static final String[] COUNT_PROJECTION = {"count(*)"}; + /* We don't want to include the download directory */ + private static final String SELECTION = + String.format("%s != %s", Media.BUCKET_ID, getDownloadBucketId()); + private static final String ORDER = String.format("%s DESC", DATE_TAKEN); + + private Context mContext; + private ArrayList<Long> mPhotos = new ArrayList<Long>(); + private ContentListener mContentListener; + private ContentObserver mContentObserver; + private boolean mContentDirty = true; + private DataManager mDataManager; + private static final Path LOCAL_IMAGE_ROOT = Path.fromString("/local/image/item"); + + public LocalPhotoSource(Context context) { + mContext = context; + mDataManager = ((GalleryApp) context.getApplicationContext()).getDataManager(); + mContentObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange) { + mContentDirty = true; + if (mContentListener != null) mContentListener.onContentDirty(); + } + }; + mContext.getContentResolver() + .registerContentObserver(CONTENT_URI, true, mContentObserver); + } + + public void close() { + mContext.getContentResolver().unregisterContentObserver(mContentObserver); + } + + @Override + public Uri getContentUri(int index) { + if (index < mPhotos.size()) { + return CONTENT_URI.buildUpon() + .appendPath(String.valueOf(mPhotos.get(index))) + .build(); + } + return null; + } + + @Override + public Bitmap getImage(int index) { + if (index >= mPhotos.size()) return null; + long id = mPhotos.get(index); + MediaItem image = (MediaItem) + mDataManager.getMediaObject(LOCAL_IMAGE_ROOT.getChild(id)); + if (image == null) return null; + + return WidgetUtils.createWidgetBitmap(image); + } + + private int[] getExponentialIndice(int total, int count) { + Random random = new Random(); + if (count > total) count = total; + HashSet<Integer> selected = new HashSet<Integer>(count); + while (selected.size() < count) { + int row = (int)(-Math.log(random.nextDouble()) * total / 2); + if (row < total) selected.add(row); + } + int values[] = new int[count]; + int index = 0; + for (int value : selected) { + values[index++] = value; + } + return values; + } + + private int getPhotoCount(ContentResolver resolver) { + Cursor cursor = resolver.query( + CONTENT_URI, COUNT_PROJECTION, SELECTION, null, null); + if (cursor == null) return 0; + try { + Utils.assertTrue(cursor.moveToNext()); + return cursor.getInt(0); + } finally { + cursor.close(); + } + } + + private boolean isContentSound(int totalCount) { + if (mPhotos.size() < Math.min(totalCount, MAX_PHOTO_COUNT)) return false; + if (mPhotos.size() == 0) return true; // totalCount is also 0 + + StringBuilder builder = new StringBuilder(); + for (Long imageId : mPhotos) { + if (builder.length() > 0) builder.append(","); + builder.append(imageId); + } + Cursor cursor = mContext.getContentResolver().query( + CONTENT_URI, COUNT_PROJECTION, + String.format("%s in (%s)", Media._ID, builder.toString()), + null, null); + if (cursor == null) return false; + try { + Utils.assertTrue(cursor.moveToNext()); + return cursor.getInt(0) == mPhotos.size(); + } finally { + cursor.close(); + } + } + + public void reload() { + if (!mContentDirty) return; + mContentDirty = false; + + ContentResolver resolver = mContext.getContentResolver(); + int photoCount = getPhotoCount(resolver); + if (isContentSound(photoCount)) return; + + int choosedIds[] = getExponentialIndice(photoCount, MAX_PHOTO_COUNT); + Arrays.sort(choosedIds); + + mPhotos.clear(); + Cursor cursor = mContext.getContentResolver().query( + CONTENT_URI, PROJECTION, SELECTION, null, ORDER); + if (cursor == null) return; + try { + for (int index : choosedIds) { + if (cursor.moveToPosition(index)) { + mPhotos.add(cursor.getLong(0)); + } + } + } finally { + cursor.close(); + } + } + + @Override + public int size() { + reload(); + return mPhotos.size(); + } + + /** + * Builds the bucket ID for the public external storage Downloads directory + * @return the bucket ID + */ + private static int getDownloadBucketId() { + String downloadsPath = Environment + .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + .getAbsolutePath(); + return GalleryUtils.getBucketId(downloadsPath); + } + + @Override + public void setContentListener(ContentListener listener) { + mContentListener = listener; + } +} diff --git a/src/com/android/gallery3d/widget/MediaSetSource.java b/src/com/android/gallery3d/widget/MediaSetSource.java new file mode 100644 index 000000000..1677f69f1 --- /dev/null +++ b/src/com/android/gallery3d/widget/MediaSetSource.java @@ -0,0 +1,113 @@ +/* + * 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.widget; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; + +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Binder; + +import java.util.ArrayList; +import java.util.Arrays; + +public class MediaSetSource implements WidgetSource, ContentListener { + private static final int CACHE_SIZE = 32; + + private static final String TAG = "MediaSetSource"; + + private MediaSet mSource; + private MediaItem mCache[] = new MediaItem[CACHE_SIZE]; + private int mCacheStart; + private int mCacheEnd; + private long mSourceVersion = MediaObject.INVALID_DATA_VERSION; + + private ContentListener mContentListener; + + public MediaSetSource(MediaSet source) { + mSource = Utils.checkNotNull(source); + mSource.addContentListener(this); + } + + @Override + public void close() { + mSource.removeContentListener(this); + } + + private void ensureCacheRange(int index) { + if (index >= mCacheStart && index < mCacheEnd) return; + + long token = Binder.clearCallingIdentity(); + try { + mCacheStart = index; + ArrayList<MediaItem> items = mSource.getMediaItem(mCacheStart, CACHE_SIZE); + mCacheEnd = mCacheStart + items.size(); + items.toArray(mCache); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public synchronized Uri getContentUri(int index) { + ensureCacheRange(index); + if (index < mCacheStart || index >= mCacheEnd) return null; + return mCache[index - mCacheStart].getContentUri(); + } + + @Override + public synchronized Bitmap getImage(int index) { + ensureCacheRange(index); + if (index < mCacheStart || index >= mCacheEnd) return null; + return WidgetUtils.createWidgetBitmap(mCache[index - mCacheStart]); + } + + @Override + public void reload() { + long version = mSource.reload(); + if (mSourceVersion != version) { + mSourceVersion = version; + mCacheStart = 0; + mCacheEnd = 0; + Arrays.fill(mCache, null); + } + } + + @Override + public void setContentListener(ContentListener listener) { + mContentListener = listener; + } + + @Override + public int size() { + long token = Binder.clearCallingIdentity(); + try { + return mSource.getMediaItemCount(); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void onContentDirty() { + if (mContentListener != null) mContentListener.onContentDirty(); + } +} diff --git a/src/com/android/gallery3d/widget/WidgetClickHandler.java b/src/com/android/gallery3d/widget/WidgetClickHandler.java new file mode 100644 index 000000000..362e4d20c --- /dev/null +++ b/src/com/android/gallery3d/widget/WidgetClickHandler.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2009 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.widget; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.Gallery; + +import android.app.Activity; +import android.content.Intent; +import android.content.res.AssetFileDescriptor; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.widget.Toast; + +public class WidgetClickHandler extends Activity { + private static final String TAG = "PhotoAppWidgetClickHandler"; + + private boolean isValidDataUri(Uri dataUri) { + if (dataUri == null) return false; + try { + AssetFileDescriptor f = getContentResolver() + .openAssetFileDescriptor(dataUri, "r"); + f.close(); + return true; + } catch (Throwable e) { + Log.w(TAG, "cannot open uri: " + dataUri, e); + return false; + } + } + + @Override + protected void onCreate(Bundle savedState) { + super.onCreate(savedState); + Intent intent = getIntent(); + if (isValidDataUri(intent.getData())) { + startActivity(new Intent(Intent.ACTION_VIEW, intent.getData())); + } else { + Toast.makeText(this, + R.string.no_such_item, Toast.LENGTH_LONG).show(); + startActivity(new Intent(this, Gallery.class)); + } + finish(); + } +} diff --git a/src/com/android/gallery3d/widget/WidgetConfigure.java b/src/com/android/gallery3d/widget/WidgetConfigure.java new file mode 100644 index 000000000..3bcd9c46e --- /dev/null +++ b/src/com/android/gallery3d/widget/WidgetConfigure.java @@ -0,0 +1,167 @@ +/* + * 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.widget; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AlbumPicker; +import com.android.gallery3d.app.CropImage; +import com.android.gallery3d.app.DialogPicker; + +import android.app.Activity; +import android.appwidget.AppWidgetManager; +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Bundle; +import android.widget.RemoteViews; + +public class WidgetConfigure extends Activity { + @SuppressWarnings("unused") + private static final String TAG = "WidgetConfigure"; + + public static final String KEY_WIDGET_TYPE = "widget-type"; + + private static final int REQUEST_WIDGET_TYPE = 1; + private static final int REQUEST_CHOOSE_ALBUM = 2; + private static final int REQUEST_CROP_IMAGE = 3; + private static final int REQUEST_GET_PHOTO = 4; + + public static final int RESULT_ERROR = RESULT_FIRST_USER; + + // Scale up the widget size since we only specified the minimized + // size of the gadget. The real size could be larger. + // Note: There is also a limit on the size of data that can be + // passed in Binder's transaction. + private static float WIDGET_SCALE_FACTOR = 1.5f; + + private int mAppWidgetId = -1; + private int mWidgetType = 0; + private Uri mPickedItem; + + @Override + protected void onCreate(Bundle bundle) { + super.onCreate(bundle); + mAppWidgetId = getIntent().getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); + + if (mAppWidgetId == -1) { + setResult(Activity.RESULT_CANCELED); + finish(); + return; + } + + if (mWidgetType == 0) { + Intent intent = new Intent(this, WidgetTypeChooser.class); + startActivityForResult(intent, REQUEST_WIDGET_TYPE); + } + } + + private void updateWidgetAndFinish(WidgetDatabaseHelper.Entry entry) { + AppWidgetManager manager = AppWidgetManager.getInstance(this); + RemoteViews views = WidgetProvider.buildWidget(this, mAppWidgetId, entry); + manager.updateAppWidget(mAppWidgetId, views); + setResult(RESULT_OK, new Intent().putExtra( + AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId)); + finish(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode != RESULT_OK) { + setResult(resultCode, new Intent().putExtra( + AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId)); + finish(); + return; + } + + if (requestCode == REQUEST_WIDGET_TYPE) { + setWidgetType(data); + } else if (requestCode == REQUEST_CHOOSE_ALBUM) { + setChoosenAlbum(data); + } else if (requestCode == REQUEST_GET_PHOTO) { + setChoosenPhoto(data); + } else if (requestCode == REQUEST_CROP_IMAGE) { + setPhotoWidget(data); + } else { + throw new AssertionError("unknown request: " + requestCode); + } + } + + private void setPhotoWidget(Intent data) { + // Store the cropped photo in our database + Bitmap bitmap = (Bitmap) data.getParcelableExtra("data"); + WidgetDatabaseHelper helper = new WidgetDatabaseHelper(this); + try { + helper.setPhoto(mAppWidgetId, mPickedItem, bitmap); + updateWidgetAndFinish(helper.getEntry(mAppWidgetId)); + } finally { + helper.close(); + } + } + + private void setChoosenPhoto(Intent data) { + Resources res = getResources(); + int widgetWidth = Math.round(WIDGET_SCALE_FACTOR + * res.getDimension(R.dimen.appwidget_width)); + int widgetHeight = Math.round(WIDGET_SCALE_FACTOR + * res.getDimension(R.dimen.appwidget_height)); + mPickedItem = data.getData(); + Intent request = new Intent(CropImage.ACTION_CROP, mPickedItem) + .putExtra(CropImage.KEY_OUTPUT_X, widgetWidth) + .putExtra(CropImage.KEY_OUTPUT_Y, widgetHeight) + .putExtra(CropImage.KEY_ASPECT_X, widgetWidth) + .putExtra(CropImage.KEY_ASPECT_Y, widgetHeight) + .putExtra(CropImage.KEY_SCALE_UP_IF_NEEDED, true) + .putExtra(CropImage.KEY_SCALE, true) + .putExtra(CropImage.KEY_RETURN_DATA, true); + startActivityForResult(request, REQUEST_CROP_IMAGE); + } + + private void setChoosenAlbum(Intent data) { + String albumPath = data.getStringExtra(AlbumPicker.KEY_ALBUM_PATH); + WidgetDatabaseHelper helper = new WidgetDatabaseHelper(this); + try { + helper.setWidget(mAppWidgetId, + WidgetDatabaseHelper.TYPE_ALBUM, albumPath); + updateWidgetAndFinish(helper.getEntry(mAppWidgetId)); + } finally { + helper.close(); + } + } + + private void setWidgetType(Intent data) { + mWidgetType = data.getIntExtra(KEY_WIDGET_TYPE, R.id.widget_type_shuffle); + if (mWidgetType == R.id.widget_type_album) { + Intent intent = new Intent(this, AlbumPicker.class); + startActivityForResult(intent, REQUEST_CHOOSE_ALBUM); + } else if (mWidgetType == R.id.widget_type_shuffle) { + WidgetDatabaseHelper helper = new WidgetDatabaseHelper(this); + try { + helper.setWidget(mAppWidgetId, WidgetDatabaseHelper.TYPE_SHUFFLE, null); + updateWidgetAndFinish(helper.getEntry(mAppWidgetId)); + } finally { + helper.close(); + } + } else { + // Explicitly send the intent to the DialogPhotoPicker + Intent request = new Intent(this, DialogPicker.class) + .setAction(Intent.ACTION_GET_CONTENT) + .setType("image/*"); + startActivityForResult(request, REQUEST_GET_PHOTO); + } + } +} diff --git a/src/com/android/gallery3d/widget/WidgetDatabaseHelper.java b/src/com/android/gallery3d/widget/WidgetDatabaseHelper.java new file mode 100644 index 000000000..d5bf22e18 --- /dev/null +++ b/src/com/android/gallery3d/widget/WidgetDatabaseHelper.java @@ -0,0 +1,187 @@ +/* + * 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.widget; + +import com.android.gallery3d.common.Utils; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.database.sqlite.SQLiteOpenHelper; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.util.Log; + +import java.io.ByteArrayOutputStream; + +public class WidgetDatabaseHelper extends SQLiteOpenHelper { + private static final String TAG = "PhotoDatabaseHelper"; + private static final String DATABASE_NAME = "launcher.db"; + + private static final int DATABASE_VERSION = 4; + + private static final String TABLE_WIDGETS = "widgets"; + + private static final String FIELD_APPWIDGET_ID = "appWidgetId"; + private static final String FIELD_IMAGE_URI = "imageUri"; + private static final String FIELD_PHOTO_BLOB = "photoBlob"; + private static final String FIELD_WIDGET_TYPE = "widgetType"; + private static final String FIELD_ALBUM_PATH = "albumPath"; + + public static final int TYPE_SINGLE_PHOTO = 0; + public static final int TYPE_SHUFFLE = 1; + public static final int TYPE_ALBUM = 2; + + private static final String[] PROJECTION = { + FIELD_WIDGET_TYPE, FIELD_IMAGE_URI, FIELD_PHOTO_BLOB, FIELD_ALBUM_PATH}; + private static final int INDEX_WIDGET_TYPE = 0; + private static final int INDEX_IMAGE_URI = 1; + private static final int INDEX_PHOTO_BLOB = 2; + private static final int INDEX_ALBUM_PATH = 3; + private static final String WHERE_CLAUSE = FIELD_APPWIDGET_ID + " = ?"; + + public static class Entry { + public int widgetId; + public int type; + public Uri imageUri; + public Bitmap image; + public String albumPath; + + private Entry(int id, Cursor cursor) { + widgetId = id; + type = cursor.getInt(INDEX_WIDGET_TYPE); + + if (type == TYPE_SINGLE_PHOTO) { + imageUri = Uri.parse(cursor.getString(INDEX_IMAGE_URI)); + image = loadBitmap(cursor, INDEX_PHOTO_BLOB); + } else if (type == TYPE_ALBUM) { + albumPath = cursor.getString(INDEX_ALBUM_PATH); + } + } + } + + public WidgetDatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + TABLE_WIDGETS + " (" + + FIELD_APPWIDGET_ID + " INTEGER PRIMARY KEY, " + + FIELD_WIDGET_TYPE + " INTEGER DEFAULT 0, " + + FIELD_IMAGE_URI + " TEXT, " + + FIELD_ALBUM_PATH + " TEXT, " + + FIELD_PHOTO_BLOB + " BLOB)"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + int version = oldVersion; + + if (version != DATABASE_VERSION) { + Log.w(TAG, "destroying all old data."); + // Table "photos" is renamed to "widget" in version 4 + db.execSQL("DROP TABLE IF EXISTS photos"); + db.execSQL("DROP TABLE IF EXISTS " + TABLE_WIDGETS); + onCreate(db); + } + } + + /** + * Store the given bitmap in this database for the given appWidgetId. + */ + public boolean setPhoto(int appWidgetId, Uri imageUri, Bitmap bitmap) { + try { + // Try go guesstimate how much space the icon will take when + // serialized to avoid unnecessary allocations/copies during + // the write. + int size = bitmap.getWidth() * bitmap.getHeight() * 4; + ByteArrayOutputStream out = new ByteArrayOutputStream(size); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + out.close(); + + ContentValues values = new ContentValues(); + values.put(FIELD_APPWIDGET_ID, appWidgetId); + values.put(FIELD_WIDGET_TYPE, TYPE_SINGLE_PHOTO); + values.put(FIELD_IMAGE_URI, imageUri.toString()); + values.put(FIELD_PHOTO_BLOB, out.toByteArray()); + + SQLiteDatabase db = getWritableDatabase(); + db.replaceOrThrow(TABLE_WIDGETS, null, values); + return true; + } catch (Throwable e) { + Log.e(TAG, "set widget photo fail", e); + return false; + } + } + + public boolean setWidget(int id, int type, String albumPath) { + try { + ContentValues values = new ContentValues(); + values.put(FIELD_APPWIDGET_ID, id); + values.put(FIELD_WIDGET_TYPE, type); + values.put(FIELD_ALBUM_PATH, Utils.ensureNotNull(albumPath)); + getWritableDatabase().replaceOrThrow(TABLE_WIDGETS, null, values); + return true; + } catch (Throwable e) { + Log.e(TAG, "set widget fail", e); + return false; + } + } + + private static Bitmap loadBitmap(Cursor cursor, int columnIndex) { + byte[] data = cursor.getBlob(columnIndex); + if (data == null) return null; + return BitmapFactory.decodeByteArray(data, 0, data.length); + } + + public Entry getEntry(int appWidgetId) { + Cursor cursor = null; + try { + SQLiteDatabase db = getReadableDatabase(); + cursor = db.query(TABLE_WIDGETS, PROJECTION, + WHERE_CLAUSE, new String[] {String.valueOf(appWidgetId)}, + null, null, null); + if (cursor == null || !cursor.moveToNext()) { + Log.e(TAG, "query fail: empty cursor: " + cursor); + return null; + } + return new Entry(appWidgetId, cursor); + } catch (Throwable e) { + Log.e(TAG, "Could not load photo from database", e); + return null; + } finally { + Utils.closeSilently(cursor); + } + } + + /** + * Remove any bitmap associated with the given appWidgetId. + */ + public void deleteEntry(int appWidgetId) { + try { + SQLiteDatabase db = getWritableDatabase(); + db.delete(TABLE_WIDGETS, WHERE_CLAUSE, + new String[] {String.valueOf(appWidgetId)}); + } catch (SQLiteException e) { + Log.e(TAG, "Could not delete photo from database", e); + } + } +}
\ No newline at end of file diff --git a/src/com/android/gallery3d/widget/WidgetProvider.java b/src/com/android/gallery3d/widget/WidgetProvider.java new file mode 100644 index 000000000..0a2fbfbe0 --- /dev/null +++ b/src/com/android/gallery3d/widget/WidgetProvider.java @@ -0,0 +1,109 @@ +/* + * 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.widget; + +import com.android.gallery3d.R; +import com.android.gallery3d.widget.WidgetDatabaseHelper.Entry; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; +import android.widget.RemoteViews; + +public class WidgetProvider extends AppWidgetProvider { + + private static final String TAG = "WidgetProvider"; + + static RemoteViews buildWidget(Context context, int id, Entry entry) { + + switch (entry.type) { + case WidgetDatabaseHelper.TYPE_ALBUM: + case WidgetDatabaseHelper.TYPE_SHUFFLE: + return buildStackWidget(context, id, entry); + case WidgetDatabaseHelper.TYPE_SINGLE_PHOTO: + return buildFrameWidget(context, id, entry); + } + throw new RuntimeException("invalid type - " + entry.type); + } + + @Override + public void onUpdate(Context context, + AppWidgetManager appWidgetManager, int[] appWidgetIds) { + WidgetDatabaseHelper helper = new WidgetDatabaseHelper(context); + try { + for (int id : appWidgetIds) { + Entry entry = helper.getEntry(id); + if (entry != null) { + RemoteViews views = buildWidget(context, id, entry); + appWidgetManager.updateAppWidget(id, views); + } else { + Log.e(TAG, "cannot load widget: " + id); + } + } + } finally { + helper.close(); + } + super.onUpdate(context, appWidgetManager, appWidgetIds); + } + + private static RemoteViews buildStackWidget(Context context, int widgetId, Entry entry) { + RemoteViews views = new RemoteViews( + context.getPackageName(), R.layout.appwidget_main); + + Intent intent = new Intent(context, WidgetService.class); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId); + intent.putExtra(WidgetService.EXTRA_WIDGET_TYPE, entry.type); + intent.putExtra(WidgetService.EXTRA_ALBUM_PATH, entry.albumPath); + intent.setData(Uri.parse("widget://gallery/" + widgetId)); + + views.setRemoteAdapter(R.id.appwidget_stack_view, intent); + views.setEmptyView(R.id.appwidget_stack_view, R.id.appwidget_empty_view); + + Intent clickIntent = new Intent(context, WidgetClickHandler.class); + PendingIntent pendingIntent = PendingIntent.getActivity( + context, 0, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT); + views.setPendingIntentTemplate(R.id.appwidget_stack_view, pendingIntent); + + return views; + } + + static RemoteViews buildFrameWidget(Context context, int appWidgetId, Entry entry) { + RemoteViews views = new RemoteViews( + context.getPackageName(), R.layout.photo_frame); + views.setImageViewBitmap(R.id.photo, entry.image); + Intent clickIntent = new Intent(context, + WidgetClickHandler.class).setData(entry.imageUri); + PendingIntent pendingClickIntent = PendingIntent.getActivity(context, 0, + clickIntent, PendingIntent.FLAG_CANCEL_CURRENT); + views.setOnClickPendingIntent(R.id.photo, pendingClickIntent); + return views; + } + + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + // Clean deleted photos out of our database + WidgetDatabaseHelper helper = new WidgetDatabaseHelper(context); + for (int appWidgetId : appWidgetIds) { + helper.deleteEntry(appWidgetId); + } + helper.close(); + } +}
\ No newline at end of file diff --git a/src/com/android/gallery3d/widget/WidgetService.java b/src/com/android/gallery3d/widget/WidgetService.java new file mode 100644 index 000000000..aa167c768 --- /dev/null +++ b/src/com/android/gallery3d/widget/WidgetService.java @@ -0,0 +1,169 @@ +/* + * 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.widget; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; + +import android.appwidget.AppWidgetManager; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.widget.RemoteViews; +import android.widget.RemoteViewsService; + +public class WidgetService extends RemoteViewsService { + + @SuppressWarnings("unused") + private static final String TAG = "GalleryAppWidgetService"; + + public static final String EXTRA_WIDGET_TYPE = "widget-type"; + public static final String EXTRA_ALBUM_PATH = "album-path"; + + @Override + public RemoteViewsFactory onGetViewFactory(Intent intent) { + int id = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); + int type = intent.getIntExtra(EXTRA_WIDGET_TYPE, 0); + String albumPath = intent.getStringExtra(EXTRA_ALBUM_PATH); + + return new PhotoRVFactory((GalleryApp) getApplicationContext(), + id, type, albumPath); + } + + private static class EmptySource implements WidgetSource { + + @Override + public int size() { + return 0; + } + + @Override + public Bitmap getImage(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public Uri getContentUri(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public void setContentListener(ContentListener listener) {} + + @Override + public void reload() {} + + @Override + public void close() {} + } + + private static class PhotoRVFactory implements + RemoteViewsService.RemoteViewsFactory, ContentListener { + + private final int mAppWidgetId; + private final int mType; + private final String mAlbumPath; + private final GalleryApp mApp; + + private WidgetSource mSource; + + public PhotoRVFactory(GalleryApp app, int id, int type, String albumPath) { + mApp = app; + mAppWidgetId = id; + mType = type; + mAlbumPath = albumPath; + } + + @Override + public void onCreate() { + if (mType == WidgetDatabaseHelper.TYPE_ALBUM) { + Path path = Path.fromString(mAlbumPath); + DataManager manager = mApp.getDataManager(); + MediaSet mediaSet = (MediaSet) manager.getMediaObject(path); + mSource = mediaSet == null + ? new EmptySource() + : new MediaSetSource(mediaSet); + } else { + mSource = new LocalPhotoSource(mApp.getAndroidContext()); + } + mSource.setContentListener(this); + AppWidgetManager.getInstance(mApp.getAndroidContext()) + .notifyAppWidgetViewDataChanged( + mAppWidgetId, R.id.appwidget_stack_view); + } + + @Override + public void onDestroy() { + mSource.close(); + mSource = null; + } + + public int getCount() { + return mSource.size(); + } + + public long getItemId(int position) { + return position; + } + + public int getViewTypeCount() { + return 1; + } + + public boolean hasStableIds() { + return true; + } + + public RemoteViews getLoadingView() { + RemoteViews rv = new RemoteViews( + mApp.getAndroidContext().getPackageName(), + R.layout.appwidget_loading_item); + rv.setProgressBar(R.id.appwidget_loading_item, 0, 0, true); + return rv; + } + + public RemoteViews getViewAt(int position) { + Bitmap bitmap = mSource.getImage(position); + if (bitmap == null) return getLoadingView(); + RemoteViews views = new RemoteViews( + mApp.getAndroidContext().getPackageName(), + R.layout.appwidget_photo_item); + views.setImageViewBitmap(R.id.appwidget_photo_item, bitmap); + views.setOnClickFillInIntent(R.id.appwidget_photo_item, new Intent() + .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + .setData(mSource.getContentUri(position))); + return views; + } + + @Override + public void onDataSetChanged() { + mSource.reload(); + } + + @Override + public void onContentDirty() { + AppWidgetManager.getInstance(mApp.getAndroidContext()) + .notifyAppWidgetViewDataChanged( + mAppWidgetId, R.id.appwidget_stack_view); + } + } +} diff --git a/src/com/android/gallery3d/widget/WidgetSource.java b/src/com/android/gallery3d/widget/WidgetSource.java new file mode 100644 index 000000000..3c73e882f --- /dev/null +++ b/src/com/android/gallery3d/widget/WidgetSource.java @@ -0,0 +1,31 @@ +/* + * 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.widget; + +import com.android.gallery3d.data.ContentListener; + +import android.graphics.Bitmap; +import android.net.Uri; + +public interface WidgetSource { + public int size(); + public Bitmap getImage(int index); + public Uri getContentUri(int index); + public void setContentListener(ContentListener listener); + public void reload(); + public void close(); +} diff --git a/src/com/android/gallery3d/widget/WidgetTypeChooser.java b/src/com/android/gallery3d/widget/WidgetTypeChooser.java new file mode 100644 index 000000000..9718e0cb2 --- /dev/null +++ b/src/com/android/gallery3d/widget/WidgetTypeChooser.java @@ -0,0 +1,59 @@ +/* + * 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.widget; + +import com.android.gallery3d.R; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.RadioGroup; +import android.widget.RadioGroup.OnCheckedChangeListener; + +public class WidgetTypeChooser extends Activity { + + private OnCheckedChangeListener mListener = new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + Intent data = new Intent() + .putExtra(WidgetConfigure.KEY_WIDGET_TYPE, checkedId); + setResult(RESULT_OK, data); + finish(); + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTitle(R.string.widget_type); + setContentView(R.layout.choose_widget_type); + RadioGroup rg = (RadioGroup) findViewById(R.id.widget_type); + rg.setOnCheckedChangeListener(mListener); + + Button cancel = (Button) findViewById(R.id.cancel); + cancel.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + setResult(RESULT_CANCELED); + finish(); + } + }); + } +} diff --git a/src/com/android/gallery3d/widget/WidgetUtils.java b/src/com/android/gallery3d/widget/WidgetUtils.java new file mode 100644 index 000000000..481bbddbc --- /dev/null +++ b/src/com/android/gallery3d/widget/WidgetUtils.java @@ -0,0 +1,80 @@ +/* + * 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.widget; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.util.ThreadPool; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Bitmap.Config; +import android.util.Log; + +public class WidgetUtils { + + private static final String TAG = "WidgetUtils"; + + private static int sStackPhotoWidth = 220; + private static int sStackPhotoHeight = 170; + + private WidgetUtils() { + } + + public static void initialize(Context context) { + Resources r = context.getResources(); + sStackPhotoWidth = r.getDimensionPixelSize(R.dimen.stack_photo_width); + sStackPhotoHeight = r.getDimensionPixelSize(R.dimen.stack_photo_height); + } + + public static Bitmap createWidgetBitmap(MediaItem image) { + Bitmap bitmap = image.requestImage(MediaItem.TYPE_THUMBNAIL) + .run(ThreadPool.JOB_CONTEXT_STUB); + if (bitmap == null) { + Log.w(TAG, "fail to get image of " + image.toString()); + return null; + } + return createWidgetBitmap(bitmap, image.getRotation()); + } + + public static Bitmap createWidgetBitmap(Bitmap bitmap, int rotation) { + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + + float scale; + if (((rotation / 90) & 1) == 0) { + scale = Math.max((float) sStackPhotoWidth / w, + (float) sStackPhotoHeight / h); + } else { + scale = Math.max((float) sStackPhotoWidth / h, + (float) sStackPhotoHeight / w); + } + + Bitmap target = Bitmap.createBitmap( + sStackPhotoWidth, sStackPhotoHeight, Config.ARGB_8888); + Canvas canvas = new Canvas(target); + canvas.translate(sStackPhotoWidth / 2, sStackPhotoHeight / 2); + canvas.rotate(rotation); + canvas.scale(scale, scale); + Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG); + canvas.drawBitmap(bitmap, -w / 2, -h / 2, paint); + return target; + } +} diff --git a/src_pd/com/android/gallery3d/picasasource/PicasaSource.java b/src_pd/com/android/gallery3d/picasasource/PicasaSource.java new file mode 100644 index 000000000..4918d72dc --- /dev/null +++ b/src_pd/com/android/gallery3d/picasasource/PicasaSource.java @@ -0,0 +1,125 @@ +/* + * 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.picasasource; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.MediaSource; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.data.PathMatcher; + +import android.content.Context; +import android.os.ParcelFileDescriptor; + +import java.io.FileNotFoundException; + +public class PicasaSource extends MediaSource { + private static final String TAG = "PicasaSource"; + + private static final int NO_MATCH = -1; + private static final int IMAGE_MEDIA_ID = 1; + + private static final int PICASA_ALBUMSET = 0; + private static final int MAP_BATCH_COUNT = 100; + + private GalleryApp mApplication; + private PathMatcher mMatcher; + + public static final Path ALBUM_PATH = Path.fromString("/picasa/all"); + + public PicasaSource(GalleryApp application) { + super("picasa"); + mApplication = application; + mMatcher = new PathMatcher(); + mMatcher.add("/picasa/all", PICASA_ALBUMSET); + mMatcher.add("/picasa/image", PICASA_ALBUMSET); + mMatcher.add("/picasa/video", PICASA_ALBUMSET); + } + + private static class EmptyAlbumSet extends MediaSet { + + public EmptyAlbumSet(Path path, long version) { + super(path, version); + } + + @Override + public String getName() { + return "picasa"; + } + + @Override + public long reload() { + return mDataVersion; + } + } + + @Override + public MediaObject createMediaObject(Path path) { + switch (mMatcher.match(path)) { + case PICASA_ALBUMSET: + return new EmptyAlbumSet(path, MediaObject.nextVersionNumber()); + default: + throw new RuntimeException("bad path: " + path); + } + } + + public static boolean isPicasaImage(MediaObject object) { + return false; + } + + public static String getImageTitle(MediaObject image) { + throw new UnsupportedOperationException(); + } + + public static int getImageSize(MediaObject image) { + throw new UnsupportedOperationException(); + } + + public static String getContentType(MediaObject image) { + throw new UnsupportedOperationException(); + } + + public static long getDateTaken(MediaObject image) { + throw new UnsupportedOperationException(); + } + + public static double getLatitude(MediaObject image) { + throw new UnsupportedOperationException(); + } + + public static double getLongitude(MediaObject image) { + throw new UnsupportedOperationException(); + } + + public static int getRotation(MediaObject image) { + throw new UnsupportedOperationException(); + } + + public static ParcelFileDescriptor openFile(Context context, MediaObject image, String mode) + throws FileNotFoundException { + throw new UnsupportedOperationException(); + } + + public static void initialize(Context context) {/*do nothing*/} + + public static void requestSync(Context context) {/*do nothing*/} + + public static void onPackageAdded(Context context, String packageName) {/*do nothing*/} + + public static void onPackageRemoved(Context context, String packageName) {/*do nothing*/} +} diff --git a/src_pd/com/android/gallery3d/settings/GallerySettings.java b/src_pd/com/android/gallery3d/settings/GallerySettings.java new file mode 100644 index 000000000..d30d755e3 --- /dev/null +++ b/src_pd/com/android/gallery3d/settings/GallerySettings.java @@ -0,0 +1,23 @@ +/* + * 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.settings; + +import android.preference.PreferenceActivity; + +public class GallerySettings extends PreferenceActivity { + private static final String TAG = "GallerySettings"; +} diff --git a/tests/Android.mk b/tests/Android.mk new file mode 100644 index 000000000..602f693e9 --- /dev/null +++ b/tests/Android.mk @@ -0,0 +1,17 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +# We only want this apk build for tests. +LOCAL_MODULE_TAGS := tests + +LOCAL_JAVA_LIBRARIES := android.test.runner + +# Include all test java files. +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_PACKAGE_NAME := Gallery2Tests +LOCAL_CERTIFICATE := media + +LOCAL_INSTRUMENTATION_FOR := Gallery2 + +include $(BUILD_PACKAGE) diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml new file mode 100644 index 000000000..010429550 --- /dev/null +++ b/tests/AndroidManifest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.gallery3d.tests"> + + <application> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation android:name="android.test.InstrumentationTestRunner" + android:targetPackage="com.android.gallery3d" + android:label="Tests for GalleryNew3D application."/> +</manifest> diff --git a/tests/src/com/android/gallery3d/anim/AnimationTest.java b/tests/src/com/android/gallery3d/anim/AnimationTest.java new file mode 100644 index 000000000..c7d5daec7 --- /dev/null +++ b/tests/src/com/android/gallery3d/anim/AnimationTest.java @@ -0,0 +1,80 @@ +/* + * 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.anim; + +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; +import android.view.animation.Interpolator; + +import junit.framework.TestCase; + +@SmallTest +public class AnimationTest extends TestCase { + private static final String TAG = "AnimationTest"; + + public void testFloatAnimation() { + FloatAnimation a = new FloatAnimation(0f, 1f, 10); // value 0 to 1.0, duration 10 + a.start(); // start animation + assertTrue(a.isActive()); // should be active now + a.calculate(0); // set start time = 0 + assertTrue(a.get() == 0); // start value should be 0 + a.calculate(1); // calculate value for time 1 + assertFloatEq(a.get(), 0.1f); + a.calculate(5); // calculate value for time 5 + assertTrue(a.get() == 0.5);// + a.calculate(9); // calculate value for time 9 + assertFloatEq(a.get(), 0.9f); + a.calculate(10); // calculate value for time 10 + assertTrue(!a.isActive()); // should be inactive now + assertTrue(a.get() == 1.0);// + a.start(); // restart + assertTrue(a.isActive()); // should be active now + a.calculate(5); // set start time = 5 + assertTrue(a.get() == 0); // start value should be 0 + a.calculate(5+9); // calculate for time 5+9 + assertFloatEq(a.get(), 0.9f); + } + + private static class MyInterpolator implements Interpolator { + public float getInterpolation(float input) { + return 4f * (input - 0.5f); // maps [0,1] to [-2,2] + } + } + + public void testInterpolator() { + FloatAnimation a = new FloatAnimation(0f, 1f, 10); // value 0 to 1.0, duration 10 + a.setInterpolator(new MyInterpolator()); + a.start(); // start animation + a.calculate(0); // set start time = 0 + assertTrue(a.get() == -2); // start value should be -2 + a.calculate(1); // calculate value for time 1 + assertFloatEq(a.get(), -1.6f); + a.calculate(5); // calculate value for time 5 + assertTrue(a.get() == 0); // + a.calculate(9); // calculate value for time 9 + assertFloatEq(a.get(), 1.6f); + a.calculate(10); // calculate value for time 10 + assertTrue(a.get() == 2); // + } + + public static void assertFloatEq(float expected, float actual) { + if (Math.abs(actual - expected) > 1e-6) { + Log.v(TAG, "expected: " + expected + ", actual: " + actual); + fail(); + } + } +} diff --git a/tests/src/com/android/gallery3d/common/BlobCacheTest.java b/tests/src/com/android/gallery3d/common/BlobCacheTest.java new file mode 100644 index 000000000..2a911c45b --- /dev/null +++ b/tests/src/com/android/gallery3d/common/BlobCacheTest.java @@ -0,0 +1,738 @@ +/* + * 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.common; + +import com.android.gallery3d.common.BlobCache; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.test.suitebuilder.annotation.MediumTest; +import android.test.suitebuilder.annotation.LargeTest; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.Random; + +public class BlobCacheTest extends AndroidTestCase { + private static final String TAG = "BlobCacheTest"; + + @SmallTest + public void testReadIntLong() { + byte[] buf = new byte[9]; + assertEquals(0, BlobCache.readInt(buf, 0)); + assertEquals(0, BlobCache.readLong(buf, 0)); + buf[0] = 1; + assertEquals(1, BlobCache.readInt(buf, 0)); + assertEquals(1, BlobCache.readLong(buf, 0)); + buf[3] = 0x7f; + assertEquals(0x7f000001, BlobCache.readInt(buf, 0)); + assertEquals(0x7f000001, BlobCache.readLong(buf, 0)); + assertEquals(0x007f0000, BlobCache.readInt(buf, 1)); + assertEquals(0x007f0000, BlobCache.readLong(buf, 1)); + buf[3] = (byte) 0x80; + buf[7] = (byte) 0xA0; + buf[0] = 0; + assertEquals(0x80000000, BlobCache.readInt(buf, 0)); + assertEquals(0xA000000080000000L, BlobCache.readLong(buf, 0)); + for (int i = 0; i < 8; i++) { + buf[i] = (byte) (0x11 * (i+8)); + } + assertEquals(0xbbaa9988, BlobCache.readInt(buf, 0)); + assertEquals(0xffeeddccbbaa9988L, BlobCache.readLong(buf, 0)); + buf[8] = 0x33; + assertEquals(0x33ffeeddccbbaa99L, BlobCache.readLong(buf, 1)); + } + + @SmallTest + public void testWriteIntLong() { + byte[] buf = new byte[8]; + BlobCache.writeInt(buf, 0, 0x12345678); + assertEquals(0x78, buf[0]); + assertEquals(0x56, buf[1]); + assertEquals(0x34, buf[2]); + assertEquals(0x12, buf[3]); + assertEquals(0x00, buf[4]); + BlobCache.writeLong(buf, 0, 0xffeeddccbbaa9988L); + for (int i = 0; i < 8; i++) { + assertEquals((byte) (0x11 * (i+8)), buf[i]); + } + } + + @MediumTest + public void testChecksum() throws IOException { + BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, true); + byte[] buf = new byte[0]; + assertEquals(0x1, bc.checkSum(buf)); + buf = new byte[1]; + assertEquals(0x10001, bc.checkSum(buf)); + buf[0] = 0x47; + assertEquals(0x480048, bc.checkSum(buf)); + buf = new byte[3]; + buf[0] = 0x10; + buf[1] = 0x30; + buf[2] = 0x01; + assertEquals(0x940042, bc.checkSum(buf)); + assertEquals(0x310031, bc.checkSum(buf, 1, 1)); + assertEquals(0x1, bc.checkSum(buf, 1, 0)); + assertEquals(0x630032, bc.checkSum(buf, 1, 2)); + buf = new byte[1024]; + for (int i = 0; i < buf.length; i++) { + buf[i] = (byte)(i*i); + } + assertEquals(0x3574a610, bc.checkSum(buf)); + bc.close(); + } + + private static final int HEADER_SIZE = 32; + private static final int DATA_HEADER_SIZE = 4; + private static final int BLOB_HEADER_SIZE = 20; + + private static final String TEST_FILE_NAME = "/sdcard/btest"; + private static final int MAX_ENTRIES = 100; + private static final int MAX_BYTES = 1000; + private static final int INDEX_SIZE = HEADER_SIZE + MAX_ENTRIES * 12 * 2; + private static final long KEY_0 = 0x1122334455667788L; + private static final long KEY_1 = 0x1122334455667789L; + private static final long KEY_2 = 0x112233445566778AL; + private static byte[] DATA_0 = new byte[10]; + private static byte[] DATA_1 = new byte[10]; + + @MediumTest + public void testBasic() throws IOException { + String name = TEST_FILE_NAME; + BlobCache bc; + File idxFile = new File(name + ".idx"); + File data0File = new File(name + ".0"); + File data1File = new File(name + ".1"); + + // Create a brand new cache. + bc = new BlobCache(name, MAX_ENTRIES, MAX_BYTES, true); + bc.close(); + + // Make sure the initial state is correct. + assertTrue(idxFile.exists()); + assertTrue(data0File.exists()); + assertTrue(data1File.exists()); + assertEquals(INDEX_SIZE, idxFile.length()); + assertEquals(DATA_HEADER_SIZE, data0File.length()); + assertEquals(DATA_HEADER_SIZE, data1File.length()); + assertEquals(0, bc.getActiveCount()); + + // Re-open it. + bc = new BlobCache(name, MAX_ENTRIES, MAX_BYTES, false); + assertNull(bc.lookup(KEY_0)); + + // insert one blob + genData(DATA_0, 1); + bc.insert(KEY_0, DATA_0); + assertSameData(DATA_0, bc.lookup(KEY_0)); + assertEquals(1, bc.getActiveCount()); + bc.close(); + + // Make sure the file size is right. + assertEquals(INDEX_SIZE, idxFile.length()); + assertEquals(DATA_HEADER_SIZE + BLOB_HEADER_SIZE + DATA_0.length, + data0File.length()); + assertEquals(DATA_HEADER_SIZE, data1File.length()); + + // Re-open it and make sure we can get the old data + bc = new BlobCache(name, MAX_ENTRIES, MAX_BYTES, false); + assertSameData(DATA_0, bc.lookup(KEY_0)); + + // insert with the same key (but using a different blob) + genData(DATA_0, 2); + bc.insert(KEY_0, DATA_0); + assertSameData(DATA_0, bc.lookup(KEY_0)); + assertEquals(1, bc.getActiveCount()); + bc.close(); + + // Make sure the file size is right. + assertEquals(INDEX_SIZE, idxFile.length()); + assertEquals(DATA_HEADER_SIZE + 2 * (BLOB_HEADER_SIZE + DATA_0.length), + data0File.length()); + assertEquals(DATA_HEADER_SIZE, data1File.length()); + + // Re-open it and make sure we can get the old data + bc = new BlobCache(name, MAX_ENTRIES, MAX_BYTES, false); + assertSameData(DATA_0, bc.lookup(KEY_0)); + + // insert another key and make sure we can get both key. + assertNull(bc.lookup(KEY_1)); + genData(DATA_1, 3); + bc.insert(KEY_1, DATA_1); + assertSameData(DATA_0, bc.lookup(KEY_0)); + assertSameData(DATA_1, bc.lookup(KEY_1)); + assertEquals(2, bc.getActiveCount()); + bc.close(); + + // Make sure the file size is right. + assertEquals(INDEX_SIZE, idxFile.length()); + assertEquals(DATA_HEADER_SIZE + 3 * (BLOB_HEADER_SIZE + DATA_0.length), + data0File.length()); + assertEquals(DATA_HEADER_SIZE, data1File.length()); + + // Re-open it and make sure we can get the old data + bc = new BlobCache(name, 100, 1000, false); + assertSameData(DATA_0, bc.lookup(KEY_0)); + assertSameData(DATA_1, bc.lookup(KEY_1)); + assertEquals(2, bc.getActiveCount()); + bc.close(); + } + + @MediumTest + public void testNegativeKey() throws IOException { + BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, true); + + // insert one blob + genData(DATA_0, 1); + bc.insert(-123, DATA_0); + assertSameData(DATA_0, bc.lookup(-123)); + bc.close(); + } + + @MediumTest + public void testEmptyBlob() throws IOException { + BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, true); + + byte[] data = new byte[0]; + bc.insert(123, data); + assertSameData(data, bc.lookup(123)); + bc.close(); + } + + @MediumTest + public void testLookupRequest() throws IOException { + BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, true); + + // insert one blob + genData(DATA_0, 1); + bc.insert(1, DATA_0); + assertSameData(DATA_0, bc.lookup(1)); + + // the same size buffer + byte[] buf = new byte[DATA_0.length]; + BlobCache.LookupRequest req = new BlobCache.LookupRequest(); + req.key = 1; + req.buffer = buf; + assertTrue(bc.lookup(req)); + assertEquals(1, req.key); + assertSame(buf, req.buffer); + assertEquals(DATA_0.length, req.length); + + // larger buffer + buf = new byte[DATA_0.length + 22]; + req = new BlobCache.LookupRequest(); + req.key = 1; + req.buffer = buf; + assertTrue(bc.lookup(req)); + assertEquals(1, req.key); + assertSame(buf, req.buffer); + assertEquals(DATA_0.length, req.length); + + // smaller buffer + buf = new byte[DATA_0.length - 1]; + req = new BlobCache.LookupRequest(); + req.key = 1; + req.buffer = buf; + assertTrue(bc.lookup(req)); + assertEquals(1, req.key); + assertNotSame(buf, req.buffer); + assertEquals(DATA_0.length, req.length); + assertSameData(DATA_0, req.buffer, DATA_0.length); + + // null buffer + req = new BlobCache.LookupRequest(); + req.key = 1; + req.buffer = null; + assertTrue(bc.lookup(req)); + assertEquals(1, req.key); + assertNotNull(req.buffer); + assertEquals(DATA_0.length, req.length); + assertSameData(DATA_0, req.buffer, DATA_0.length); + + bc.close(); + } + + @MediumTest + public void testKeyCollision() throws IOException { + BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, true); + + for (int i = 0; i < MAX_ENTRIES / 2; i++) { + genData(DATA_0, i); + long key = KEY_1 + i * MAX_ENTRIES; + bc.insert(key, DATA_0); + } + + for (int i = 0; i < MAX_ENTRIES / 2; i++) { + genData(DATA_0, i); + long key = KEY_1 + i * MAX_ENTRIES; + assertSameData(DATA_0, bc.lookup(key)); + } + bc.close(); + } + + @MediumTest + public void testRegionFlip() throws IOException { + String name = TEST_FILE_NAME; + BlobCache bc; + File idxFile = new File(name + ".idx"); + File data0File = new File(name + ".0"); + File data1File = new File(name + ".1"); + + // Create a brand new cache. + bc = new BlobCache(name, MAX_ENTRIES, MAX_BYTES, true); + + // This is the number of blobs fits into a region. + int maxFit = (MAX_BYTES - DATA_HEADER_SIZE) / + (BLOB_HEADER_SIZE + DATA_0.length); + + for (int k = 0; k < maxFit; k++) { + genData(DATA_0, k); + bc.insert(k, DATA_0); + } + assertEquals(maxFit, bc.getActiveCount()); + + // Make sure the file size is right. + assertEquals(INDEX_SIZE, idxFile.length()); + assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length), + data0File.length()); + assertEquals(DATA_HEADER_SIZE, data1File.length()); + + // Now insert another one and let it flip. + genData(DATA_0, 777); + bc.insert(KEY_1, DATA_0); + assertEquals(1, bc.getActiveCount()); + + assertEquals(INDEX_SIZE, idxFile.length()); + assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length), + data0File.length()); + assertEquals(DATA_HEADER_SIZE + 1 * (BLOB_HEADER_SIZE + DATA_0.length), + data1File.length()); + + // Make sure we can find the new data + assertSameData(DATA_0, bc.lookup(KEY_1)); + + // Now find an old blob + int old = maxFit / 2; + genData(DATA_0, old); + assertSameData(DATA_0, bc.lookup(old)); + assertEquals(2, bc.getActiveCount()); + + // Observed data is copied. + assertEquals(INDEX_SIZE, idxFile.length()); + assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length), + data0File.length()); + assertEquals(DATA_HEADER_SIZE + 2 * (BLOB_HEADER_SIZE + DATA_0.length), + data1File.length()); + + // Now copy everything over (except we should have no space for the last one) + assertTrue(old < maxFit - 1); + for (int k = 0; k < maxFit; k++) { + genData(DATA_0, k); + assertSameData(DATA_0, bc.lookup(k)); + } + assertEquals(maxFit, bc.getActiveCount()); + + // Now both file should be full. + assertEquals(INDEX_SIZE, idxFile.length()); + assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length), + data0File.length()); + assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length), + data1File.length()); + + // Now insert one to make it flip. + genData(DATA_0, 888); + bc.insert(KEY_2, DATA_0); + assertEquals(1, bc.getActiveCount()); + + // Check the size after the second flip. + assertEquals(INDEX_SIZE, idxFile.length()); + assertEquals(DATA_HEADER_SIZE + 1 * (BLOB_HEADER_SIZE + DATA_0.length), + data0File.length()); + assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length), + data1File.length()); + + // Now the last key should be gone. + assertNull(bc.lookup(maxFit - 1)); + + // But others should remain + for (int k = 0; k < maxFit - 1; k++) { + genData(DATA_0, k); + assertSameData(DATA_0, bc.lookup(k)); + } + + assertEquals(maxFit, bc.getActiveCount()); + genData(DATA_0, 777); + assertSameData(DATA_0, bc.lookup(KEY_1)); + genData(DATA_0, 888); + assertSameData(DATA_0, bc.lookup(KEY_2)); + assertEquals(maxFit, bc.getActiveCount()); + + // Now two files should be full. + assertEquals(INDEX_SIZE, idxFile.length()); + assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length), + data0File.length()); + assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length), + data1File.length()); + + bc.close(); + } + + @MediumTest + public void testEntryLimit() throws IOException { + String name = TEST_FILE_NAME; + BlobCache bc; + File idxFile = new File(name + ".idx"); + File data0File = new File(name + ".0"); + File data1File = new File(name + ".1"); + int maxEntries = 10; + int maxFit = maxEntries / 2; + int indexSize = HEADER_SIZE + maxEntries * 12 * 2; + + // Create a brand new cache with a small entry limit. + bc = new BlobCache(name, maxEntries, MAX_BYTES, true); + + // Fill to just before flipping + for (int i = 0; i < maxFit; i++) { + genData(DATA_0, i); + bc.insert(i, DATA_0); + } + assertEquals(maxFit, bc.getActiveCount()); + + // Check the file size. + assertEquals(indexSize, idxFile.length()); + assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length), + data0File.length()); + assertEquals(DATA_HEADER_SIZE, data1File.length()); + + // Insert one and make it flip + genData(DATA_0, 777); + bc.insert(777, DATA_0); + assertEquals(1, bc.getActiveCount()); + + // Check the file size. + assertEquals(indexSize, idxFile.length()); + assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length), + data0File.length()); + assertEquals(DATA_HEADER_SIZE + 1 * (BLOB_HEADER_SIZE + DATA_0.length), + data1File.length()); + bc.close(); + } + + @LargeTest + public void testDataIntegrity() throws IOException { + String name = TEST_FILE_NAME; + File idxFile = new File(name + ".idx"); + File data0File = new File(name + ".0"); + File data1File = new File(name + ".1"); + RandomAccessFile f; + + Log.v(TAG, "It should be readable if the content is not changed."); + prepareNewCache(); + f = new RandomAccessFile(data0File, "rw"); + f.seek(1); + byte b = f.readByte(); + f.seek(1); + f.write(b); + f.close(); + assertReadable(); + + Log.v(TAG, "Change the data file magic field"); + prepareNewCache(); + f = new RandomAccessFile(data0File, "rw"); + f.seek(1); + f.write(0xFF); + f.close(); + assertUnreadable(); + + prepareNewCache(); + f = new RandomAccessFile(data1File, "rw"); + f.write(0xFF); + f.close(); + assertUnreadable(); + + Log.v(TAG, "Change the blob key"); + prepareNewCache(); + f = new RandomAccessFile(data0File, "rw"); + f.seek(4); + f.write(0x00); + f.close(); + assertUnreadable(); + + Log.v(TAG, "Change the blob checksum"); + prepareNewCache(); + f = new RandomAccessFile(data0File, "rw"); + f.seek(4 + 8); + f.write(0x00); + f.close(); + assertUnreadable(); + + Log.v(TAG, "Change the blob offset"); + prepareNewCache(); + f = new RandomAccessFile(data0File, "rw"); + f.seek(4 + 12); + f.write(0x20); + f.close(); + assertUnreadable(); + + Log.v(TAG, "Change the blob length: some other value"); + prepareNewCache(); + f = new RandomAccessFile(data0File, "rw"); + f.seek(4 + 16); + f.write(0x20); + f.close(); + assertUnreadable(); + + Log.v(TAG, "Change the blob length: -1"); + prepareNewCache(); + f = new RandomAccessFile(data0File, "rw"); + f.seek(4 + 16); + f.writeInt(0xFFFFFFFF); + f.close(); + assertUnreadable(); + + Log.v(TAG, "Change the blob length: big value"); + prepareNewCache(); + f = new RandomAccessFile(data0File, "rw"); + f.seek(4 + 16); + f.writeInt(0xFFFFFF00); + f.close(); + assertUnreadable(); + + Log.v(TAG, "Change the blob content"); + prepareNewCache(); + f = new RandomAccessFile(data0File, "rw"); + f.seek(4 + 20); + f.write(0x01); + f.close(); + assertUnreadable(); + + Log.v(TAG, "Change the index magic"); + prepareNewCache(); + f = new RandomAccessFile(idxFile, "rw"); + f.seek(1); + f.write(0x00); + f.close(); + assertUnreadable(); + + Log.v(TAG, "Change the active region"); + prepareNewCache(); + f = new RandomAccessFile(idxFile, "rw"); + f.seek(12); + f.write(0x01); + f.close(); + assertUnreadable(); + + Log.v(TAG, "Change the reserved data"); + prepareNewCache(); + f = new RandomAccessFile(idxFile, "rw"); + f.seek(24); + f.write(0x01); + f.close(); + assertUnreadable(); + + Log.v(TAG, "Change the checksum"); + prepareNewCache(); + f = new RandomAccessFile(idxFile, "rw"); + f.seek(29); + f.write(0x00); + f.close(); + assertUnreadable(); + + Log.v(TAG, "Change the key"); + prepareNewCache(); + f = new RandomAccessFile(idxFile, "rw"); + f.seek(32 + 12 * (KEY_1 % MAX_ENTRIES)); + f.write(0x00); + f.close(); + assertUnreadable(); + + Log.v(TAG, "Change the offset"); + prepareNewCache(); + f = new RandomAccessFile(idxFile, "rw"); + f.seek(32 + 12 * (KEY_1 % MAX_ENTRIES) + 8); + f.write(0x05); + f.close(); + assertUnreadable(); + + Log.v(TAG, "Change the offset"); + prepareNewCache(); + f = new RandomAccessFile(idxFile, "rw"); + f.seek(32 + 12 * (KEY_1 % MAX_ENTRIES) + 8 + 3); + f.write(0xFF); + f.close(); + assertUnreadable(); + + Log.v(TAG, "Garbage index"); + prepareNewCache(); + f = new RandomAccessFile(idxFile, "rw"); + int n = (int) idxFile.length(); + f.seek(32); + byte[] garbage = new byte[1024]; + for (int i = 0; i < garbage.length; i++) { + garbage[i] = (byte) 0x80; + } + int i = 32; + while (i < n) { + int todo = Math.min(garbage.length, n - i); + f.write(garbage, 0, todo); + i += todo; + } + f.close(); + assertUnreadable(); + } + + // Create a brand new cache and put one entry into it. + private void prepareNewCache() throws IOException { + BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, true); + genData(DATA_0, 777); + bc.insert(KEY_1, DATA_0); + bc.close(); + } + + private void assertReadable() throws IOException { + BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, false); + genData(DATA_0, 777); + assertSameData(DATA_0, bc.lookup(KEY_1)); + bc.close(); + } + + private void assertUnreadable() throws IOException { + BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, false); + genData(DATA_0, 777); + assertNull(bc.lookup(KEY_1)); + bc.close(); + } + + @LargeTest + public void testRandomSize() throws IOException { + BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, true); + + // Random size test + Random rand = new Random(0); + for (int i = 0; i < 100; i++) { + byte[] data = new byte[rand.nextInt(MAX_BYTES*12/10)]; + try { + bc.insert(rand.nextLong(), data); + if (data.length > MAX_BYTES - 4 - 20) fail(); + } catch (RuntimeException ex) { + if (data.length <= MAX_BYTES - 4 - 20) fail(); + } + } + + bc.close(); + } + + @LargeTest + public void testBandwidth() throws IOException { + BlobCache bc = new BlobCache(TEST_FILE_NAME, 1000, 10000000, true); + + // Write + int count = 0; + byte[] data = new byte[20000]; + long t0 = System.nanoTime(); + for (int i = 0; i < 1000; i++) { + bc.insert(i, data); + count += data.length; + } + bc.syncAll(); + float delta = (System.nanoTime() - t0) * 1e-3f; + Log.v(TAG, "write bandwidth = " + (count / delta) + " M/s"); + + // Copy over + BlobCache.LookupRequest req = new BlobCache.LookupRequest(); + count = 0; + t0 = System.nanoTime(); + for (int i = 0; i < 1000; i++) { + req.key = i; + req.buffer = data; + if (bc.lookup(req)) { + count += req.length; + } + } + bc.syncAll(); + delta = (System.nanoTime() - t0) * 1e-3f; + Log.v(TAG, "copy over bandwidth = " + (count / delta) + " M/s"); + + // Read + count = 0; + t0 = System.nanoTime(); + for (int i = 0; i < 1000; i++) { + req.key = i; + req.buffer = data; + if (bc.lookup(req)) { + count += req.length; + } + } + bc.syncAll(); + delta = (System.nanoTime() - t0) * 1e-3f; + Log.v(TAG, "read bandwidth = " + (count / delta) + " M/s"); + + bc.close(); + } + + @LargeTest + public void testSmallSize() throws IOException { + BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, 40, true); + + // Small size test + Random rand = new Random(0); + for (int i = 0; i < 100; i++) { + byte[] data = new byte[rand.nextInt(3)]; + bc.insert(rand.nextLong(), data); + } + + bc.close(); + } + + @LargeTest + public void testManyEntries() throws IOException { + BlobCache bc = new BlobCache(TEST_FILE_NAME, 1, MAX_BYTES, true); + + // Many entries test + Random rand = new Random(0); + for (int i = 0; i < 100; i++) { + byte[] data = new byte[rand.nextInt(10)]; + } + + bc.close(); + } + + private void genData(byte[] data, int seed) { + for(int i = 0; i < data.length; i++) { + data[i] = (byte) (seed * i); + } + } + + private void assertSameData(byte[] data1, byte[] data2) { + if (data1 == null && data2 == null) return; + if (data1 == null || data2 == null) fail(); + if (data1.length != data2.length) fail(); + for (int i = 0; i < data1.length; i++) { + if (data1[i] != data2[i]) fail(); + } + } + + private void assertSameData(byte[] data1, byte[] data2, int n) { + if (data1 == null || data2 == null) fail(); + for (int i = 0; i < n; i++) { + if (data1[i] != data2[i]) fail(); + } + } +} diff --git a/tests/src/com/android/gallery3d/common/UtilsTest.java b/tests/src/com/android/gallery3d/common/UtilsTest.java new file mode 100644 index 000000000..b3552444b --- /dev/null +++ b/tests/src/com/android/gallery3d/common/UtilsTest.java @@ -0,0 +1,244 @@ +/* + * 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.common; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; + +public class UtilsTest extends AndroidTestCase { + private static final String TAG = "UtilsTest"; + + private static final int [] testData = new int [] { + /* outWidth, outHeight, minSideLength, maxNumOfPixels, sample size */ + 1, 1, BitmapUtils.UNCONSTRAINED, BitmapUtils.UNCONSTRAINED, 1, + 1, 1, 1, 1, 1, + 100, 100, 100, 10000, 1, + 100, 100, 100, 2500, 2, + 99, 66, 33, 10000, 2, + 66, 99, 33, 10000, 2, + 99, 66, 34, 10000, 1, + 99, 66, 22, 10000, 4, + 99, 66, 16, 10000, 4, + + 10000, 10000, 20000, 1000000, 16, + + 100, 100, 100, 10000, 1, // 1 + 100, 100, 50, 10000, 2, // 2 + 100, 100, 30, 10000, 4, // 3->4 + 100, 100, 22, 10000, 4, // 4 + 100, 100, 20, 10000, 8, // 5->8 + 100, 100, 11, 10000, 16, // 9->16 + 100, 100, 5, 10000, 24, // 20->24 + 100, 100, 2, 10000, 56, // 50->56 + + 100, 100, 100, 10000 - 1, 2, // a bit less than 1 + 100, 100, 100, 10000 / (2 * 2) - 1, 4, // a bit less than 2 + 100, 100, 100, 10000 / (3 * 3) - 1, 4, // a bit less than 3 + 100, 100, 100, 10000 / (4 * 4) - 1, 8, // a bit less than 4 + 100, 100, 100, 10000 / (8 * 8) - 1, 16, // a bit less than 8 + 100, 100, 100, 10000 / (16 * 16) - 1, 24, // a bit less than 16 + 100, 100, 100, 10000 / (24 * 24) - 1, 32, // a bit less than 24 + 100, 100, 100, 10000 / (32 * 32) - 1, 40, // a bit less than 32 + + 640, 480, 480, BitmapUtils.UNCONSTRAINED, 1, // 1 + 640, 480, 240, BitmapUtils.UNCONSTRAINED, 2, // 2 + 640, 480, 160, BitmapUtils.UNCONSTRAINED, 4, // 3->4 + 640, 480, 120, BitmapUtils.UNCONSTRAINED, 4, // 4 + 640, 480, 96, BitmapUtils.UNCONSTRAINED, 8, // 5->8 + 640, 480, 80, BitmapUtils.UNCONSTRAINED, 8, // 6->8 + 640, 480, 60, BitmapUtils.UNCONSTRAINED, 8, // 8 + 640, 480, 48, BitmapUtils.UNCONSTRAINED, 16, // 10->16 + 640, 480, 40, BitmapUtils.UNCONSTRAINED, 16, // 12->16 + 640, 480, 30, BitmapUtils.UNCONSTRAINED, 16, // 16 + 640, 480, 24, BitmapUtils.UNCONSTRAINED, 24, // 20->24 + 640, 480, 20, BitmapUtils.UNCONSTRAINED, 24, // 24 + 640, 480, 16, BitmapUtils.UNCONSTRAINED, 32, // 30->32 + 640, 480, 12, BitmapUtils.UNCONSTRAINED, 40, // 40 + 640, 480, 10, BitmapUtils.UNCONSTRAINED, 48, // 48 + 640, 480, 8, BitmapUtils.UNCONSTRAINED, 64, // 60->64 + 640, 480, 6, BitmapUtils.UNCONSTRAINED, 80, // 80 + 640, 480, 4, BitmapUtils.UNCONSTRAINED, 120, // 120 + 640, 480, 3, BitmapUtils.UNCONSTRAINED, 160, // 160 + 640, 480, 2, BitmapUtils.UNCONSTRAINED, 240, // 240 + 640, 480, 1, BitmapUtils.UNCONSTRAINED, 480, // 480 + + 640, 480, BitmapUtils.UNCONSTRAINED, BitmapUtils.UNCONSTRAINED, 1, + 640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480, 1, // 1 + 640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 - 1, 2, // a bit less than 1 + 640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / 4, 2, // 2 + 640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / 4 - 1, 4, // a bit less than 2 + 640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / 9, 4, // 3 + 640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / 9 - 1, 4, // a bit less than 3 + 640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / 16, 4, // 4 + 640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / 16 - 1, 8, // a bit less than 4 + 640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / 64, 8, // 8 + 640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / 64 - 1, 16, // a bit less than 8 + 640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / 256, 16, // 16 + 640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / 256 - 1, 24, // a bit less than 16 + 640, 480, BitmapUtils.UNCONSTRAINED, 640 * 480 / (24 * 24) - 1, 32, // a bit less than 24 + }; + + @SmallTest + public void testComputeSampleSize() { + + for (int i = 0; i < testData.length; i += 5) { + int w = testData[i]; + int h = testData[i + 1]; + int minSide = testData[i + 2]; + int maxPixels = testData[i + 3]; + int sampleSize = testData[i + 4]; + int result = BitmapUtils.computeSampleSize(w, h, minSide, maxPixels); + if (result != sampleSize) { + Log.v(TAG, w + "x" + h + ", minSide = " + minSide + ", maxPixels = " + + maxPixels + ", sampleSize = " + sampleSize + ", result = " + + result); + } + assertTrue(sampleSize == result); + } + } + + public void testAssert() { + // This should not throw an exception. + Utils.assertTrue(true); + + // This should throw an exception. + try { + Utils.assertTrue(false); + fail(); + } catch (AssertionError ex) { + // expected. + } + } + + public void testCheckNotNull() { + // These should not throw an expection. + Utils.checkNotNull(new Object()); + Utils.checkNotNull(0); + Utils.checkNotNull(""); + + // This should throw an expection. + try { + Utils.checkNotNull(null); + fail(); + } catch (NullPointerException ex) { + // expected. + } + } + + public void testEquals() { + Object a = new Object(); + Object b = new Object(); + + assertTrue(Utils.equals(null, null)); + assertTrue(Utils.equals(a, a)); + assertFalse(Utils.equals(null, a)); + assertFalse(Utils.equals(a, null)); + assertFalse(Utils.equals(a, b)); + } + + public void testIsPowerOf2() { + for (int i = 0; i < 31; i++) { + int v = (1 << i); + assertTrue(Utils.isPowerOf2(v)); + } + + int[] f = new int[] {3, 5, 6, 7, 9, 10, 65535, Integer.MAX_VALUE - 1, + Integer.MAX_VALUE }; + for (int v : f) { + assertFalse(Utils.isPowerOf2(v)); + } + + int[] e = new int[] {0, -1, -2, -4, -65536, Integer.MIN_VALUE + 1, + Integer.MIN_VALUE }; + for (int v : e) { + try { + Utils.isPowerOf2(v); + fail(); + } catch (IllegalArgumentException ex) { + // expected. + } + } + } + + public void testNextPowerOf2() { + int[] q = new int[] {1, 2, 3, 4, 5, 6, 10, 65535, (1 << 30) - 1, (1 << 30)}; + int[] a = new int[] {1, 2, 4, 4, 8, 8, 16, 65536, (1 << 30) , (1 << 30)}; + + for (int i = 0; i < q.length; i++) { + assertEquals(a[i], Utils.nextPowerOf2(q[i])); + } + + int[] e = new int[] {0, -1, -2, -4, -65536, (1 << 30) + 1, Integer.MAX_VALUE}; + + for (int v : e) { + try { + Utils.nextPowerOf2(v); + fail(); + } catch (IllegalArgumentException ex) { + // expected. + } + } + } + + public void testDistance() { + assertFloatEq(0f, Utils.distance(0, 0, 0, 0)); + assertFloatEq(1f, Utils.distance(0, 1, 0, 0)); + assertFloatEq(1f, Utils.distance(0, 0, 0, 1)); + assertFloatEq(2f, Utils.distance(1, 2, 3, 2)); + assertFloatEq(5f, Utils.distance(1, 2, 1 + 3, 2 + 4)); + assertFloatEq(5f, Utils.distance(1, 2, 1 + 3, 2 + 4)); + assertFloatEq(Float.MAX_VALUE, Utils.distance(Float.MAX_VALUE, 0, 0, 0)); + } + + public void testClamp() { + assertEquals(1000, Utils.clamp(300, 1000, 2000)); + assertEquals(1300, Utils.clamp(1300, 1000, 2000)); + assertEquals(2000, Utils.clamp(2300, 1000, 2000)); + + assertEquals(0.125f, Utils.clamp(0.1f, 0.125f, 0.5f)); + assertEquals(0.25f, Utils.clamp(0.25f, 0.125f, 0.5f)); + assertEquals(0.5f, Utils.clamp(0.9f, 0.125f, 0.5f)); + } + + public void testIsOpaque() { + assertTrue(Utils.isOpaque(0xFF000000)); + assertTrue(Utils.isOpaque(0xFFFFFFFF)); + assertTrue(Utils.isOpaque(0xFF123456)); + + assertFalse(Utils.isOpaque(0xFEFFFFFF)); + assertFalse(Utils.isOpaque(0x8FFFFFFF)); + assertFalse(Utils.isOpaque(0x00FF0000)); + assertFalse(Utils.isOpaque(0x5500FF00)); + assertFalse(Utils.isOpaque(0xAA0000FF)); + } + + public static void testSwap() { + Integer[] a = {1, 2, 3}; + Utils.swap(a, 0, 2); + assertEquals(a[0].intValue(), 3); + assertEquals(a[1].intValue(), 2); + assertEquals(a[2].intValue(), 1); + } + + public static void assertFloatEq(float expected, float actual) { + if (Math.abs(actual - expected) > 1e-6) { + Log.v(TAG, "expected: " + expected + ", actual: " + actual); + fail(); + } + } +} diff --git a/tests/src/com/android/gallery3d/data/GalleryAppMock.java b/tests/src/com/android/gallery3d/data/GalleryAppMock.java new file mode 100644 index 000000000..bbc569238 --- /dev/null +++ b/tests/src/com/android/gallery3d/data/GalleryAppMock.java @@ -0,0 +1,50 @@ +/* + * 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.data; + +import android.content.ContentResolver; +import android.content.Context; +import android.os.Looper; + +import com.android.gallery3d.ui.GLRoot; +import com.android.gallery3d.ui.GLRootStub; + +class GalleryAppMock extends GalleryAppStub { + GLRoot mGLRoot = new GLRootStub(); + DataManager mDataManager = new DataManager(this); + ContentResolver mResolver; + Context mContext; + Looper mMainLooper; + + GalleryAppMock(Context context, + ContentResolver resolver, Looper mainLooper) { + mContext = context; + mResolver = resolver; + mMainLooper = mainLooper; + } + + @Override + public GLRoot getGLRoot() { return mGLRoot; } + @Override + public DataManager getDataManager() { return mDataManager; } + @Override + public Context getAndroidContext() { return mContext; } + @Override + public ContentResolver getContentResolver() { return mResolver; } + @Override + public Looper getMainLooper() { return mMainLooper; } +} diff --git a/tests/src/com/android/gallery3d/data/GalleryAppStub.java b/tests/src/com/android/gallery3d/data/GalleryAppStub.java new file mode 100644 index 000000000..36075f435 --- /dev/null +++ b/tests/src/com/android/gallery3d/data/GalleryAppStub.java @@ -0,0 +1,47 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.app.StateManager; +import com.android.gallery3d.ui.GLRoot; +import com.android.gallery3d.ui.PositionRepository; +import com.android.gallery3d.util.ThreadPool; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.os.Looper; + +class GalleryAppStub implements GalleryApp { + public ImageCacheService getImageCacheService() { return null; } + public StateManager getStateManager() { return null; } + public DataManager getDataManager() { return null; } + public DownloadUtils getDownloadService() { return null; } + public DecodeUtils getDecodeService() { return null; } + + public GLRoot getGLRoot() { return null; } + public PositionRepository getPositionRepository() { return null; } + + public Context getAndroidContext() { return null; } + + public Looper getMainLooper() { return null; } + public Resources getResources() { return null; } + public ContentResolver getContentResolver() { return null; } + public ThreadPool getThreadPool() { return null; } + public DownloadCache getDownloadCache() { return null; } +} diff --git a/tests/src/com/android/gallery3d/data/LocalDataTest.java b/tests/src/com/android/gallery3d/data/LocalDataTest.java new file mode 100644 index 000000000..8f6a46b8e --- /dev/null +++ b/tests/src/com/android/gallery3d/data/LocalDataTest.java @@ -0,0 +1,461 @@ +/* + * 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.data; + +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.os.Looper; +import android.test.AndroidTestCase; +import android.test.mock.MockContentProvider; +import android.test.mock.MockContentResolver; +import android.test.suitebuilder.annotation.MediumTest; +import android.util.Log; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class LocalDataTest extends AndroidTestCase { + @SuppressWarnings("unused") + private static final String TAG = "LocalDataTest"; + private static final long DEFAULT_TIMEOUT = 1000; // one second + + @MediumTest + public void testLocalAlbum() throws Exception { + new TestZeroImage().run(); + new TestOneImage().run(); + new TestMoreImages().run(); + new TestZeroVideo().run(); + new TestOneVideo().run(); + new TestMoreVideos().run(); + new TestDeleteOneImage().run(); + new TestDeleteOneAlbum().run(); + } + + abstract class TestLocalAlbumBase { + private boolean mIsImage; + protected GalleryAppStub mApp; + protected LocalAlbumSet mAlbumSet; + + TestLocalAlbumBase(boolean isImage) { + mIsImage = isImage; + } + + public void run() throws Exception { + SQLiteDatabase db = SQLiteDatabase.create(null); + prepareData(db); + mApp = newGalleryContext(db, Looper.getMainLooper()); + Path.clearAll(); + Path path = Path.fromString( + mIsImage ? "/local/image" : "/local/video"); + mAlbumSet = new LocalAlbumSet(path, mApp); + mAlbumSet.reload(); + verifyResult(); + } + + abstract void prepareData(SQLiteDatabase db); + abstract void verifyResult() throws Exception; + } + + abstract class TestLocalImageAlbum extends TestLocalAlbumBase { + TestLocalImageAlbum() { + super(true); + } + } + + abstract class TestLocalVideoAlbum extends TestLocalAlbumBase { + TestLocalVideoAlbum() { + super(false); + } + } + + class TestZeroImage extends TestLocalImageAlbum { + @Override + public void prepareData(SQLiteDatabase db) { + createImageTable(db); + } + + @Override + public void verifyResult() { + assertEquals(0, mAlbumSet.getMediaItemCount()); + assertEquals(0, mAlbumSet.getSubMediaSetCount()); + assertEquals(0, mAlbumSet.getTotalMediaItemCount()); + } + } + + class TestOneImage extends TestLocalImageAlbum { + @Override + public void prepareData(SQLiteDatabase db) { + createImageTable(db); + insertImageData(db); + } + + @Override + public void verifyResult() { + assertEquals(0, mAlbumSet.getMediaItemCount()); + assertEquals(1, mAlbumSet.getSubMediaSetCount()); + assertEquals(1, mAlbumSet.getTotalMediaItemCount()); + MediaSet sub = mAlbumSet.getSubMediaSet(0); + assertEquals(1, sub.getMediaItemCount()); + assertEquals(0, sub.getSubMediaSetCount()); + LocalMediaItem item = (LocalMediaItem) sub.getMediaItem(0, 1).get(0); + assertEquals(1, item.id); + assertEquals("IMG_0072", item.caption); + assertEquals("image/jpeg", item.mimeType); + assertEquals(12.0, item.latitude); + assertEquals(34.0, item.longitude); + assertEquals(0xD000, item.dateTakenInMs); + assertEquals(1280395646L, item.dateAddedInSec); + assertEquals(1275934796L, item.dateModifiedInSec); + assertEquals("/mnt/sdcard/DCIM/100CANON/IMG_0072.JPG", item.filePath); + } + } + + class TestMoreImages extends TestLocalImageAlbum { + @Override + public void prepareData(SQLiteDatabase db) { + // Albums are sorted by names, and items are sorted by + // dateTimeTaken (descending) + createImageTable(db); + // bucket 0xB000 + insertImageData(db, 1000, 0xB000, "second"); // id 1 + insertImageData(db, 2000, 0xB000, "second"); // id 2 + // bucket 0xB001 + insertImageData(db, 3000, 0xB001, "first"); // id 3 + } + + @Override + public void verifyResult() { + assertEquals(0, mAlbumSet.getMediaItemCount()); + assertEquals(2, mAlbumSet.getSubMediaSetCount()); + assertEquals(3, mAlbumSet.getTotalMediaItemCount()); + + MediaSet first = mAlbumSet.getSubMediaSet(0); + assertEquals(1, first.getMediaItemCount()); + LocalMediaItem item = (LocalMediaItem) first.getMediaItem(0, 1).get(0); + assertEquals(3, item.id); + assertEquals(3000L, item.dateTakenInMs); + + MediaSet second = mAlbumSet.getSubMediaSet(1); + assertEquals(2, second.getMediaItemCount()); + item = (LocalMediaItem) second.getMediaItem(0, 1).get(0); + assertEquals(2, item.id); + assertEquals(2000L, item.dateTakenInMs); + item = (LocalMediaItem) second.getMediaItem(1, 1).get(0); + assertEquals(1, item.id); + assertEquals(1000L, item.dateTakenInMs); + } + } + + class OnContentDirtyLatch implements ContentListener { + private CountDownLatch mLatch = new CountDownLatch(1); + + public void onContentDirty() { + mLatch.countDown(); + } + + public boolean isOnContentDirtyBeCalled(long timeout) + throws InterruptedException { + return mLatch.await(timeout, TimeUnit.MILLISECONDS); + } + } + + class TestDeleteOneAlbum extends TestLocalImageAlbum { + @Override + public void prepareData(SQLiteDatabase db) { + // Albums are sorted by names, and items are sorted by + // dateTimeTaken (descending) + createImageTable(db); + // bucket 0xB000 + insertImageData(db, 1000, 0xB000, "second"); // id 1 + insertImageData(db, 2000, 0xB000, "second"); // id 2 + // bucket 0xB001 + insertImageData(db, 3000, 0xB001, "first"); // id 3 + } + + @Override + public void verifyResult() throws Exception { + MediaSet sub = mAlbumSet.getSubMediaSet(1); // "second" + assertEquals(2, mAlbumSet.getSubMediaSetCount()); + OnContentDirtyLatch latch = new OnContentDirtyLatch(); + sub.addContentListener(latch); + assertTrue((sub.getSupportedOperations() & MediaSet.SUPPORT_DELETE) != 0); + sub.delete(); + mAlbumSet.fakeChange(); + latch.isOnContentDirtyBeCalled(DEFAULT_TIMEOUT); + mAlbumSet.reload(); + assertEquals(1, mAlbumSet.getSubMediaSetCount()); + } + } + + class TestDeleteOneImage extends TestLocalImageAlbum { + + @Override + public void prepareData(SQLiteDatabase db) { + createImageTable(db); + insertImageData(db); + } + + @Override + public void verifyResult() { + MediaSet sub = mAlbumSet.getSubMediaSet(0); + LocalMediaItem item = (LocalMediaItem) sub.getMediaItem(0, 1).get(0); + assertEquals(1, sub.getMediaItemCount()); + assertTrue((sub.getSupportedOperations() & MediaSet.SUPPORT_DELETE) != 0); + sub.delete(); + sub.reload(); + assertEquals(0, sub.getMediaItemCount()); + } + } + + static void createImageTable(SQLiteDatabase db) { + // This is copied from MediaProvider + db.execSQL("CREATE TABLE IF NOT EXISTS images (" + + "_id INTEGER PRIMARY KEY," + + "_data TEXT," + + "_size INTEGER," + + "_display_name TEXT," + + "mime_type TEXT," + + "title TEXT," + + "date_added INTEGER," + + "date_modified INTEGER," + + "description TEXT," + + "picasa_id TEXT," + + "isprivate INTEGER," + + "latitude DOUBLE," + + "longitude DOUBLE," + + "datetaken INTEGER," + + "orientation INTEGER," + + "mini_thumb_magic INTEGER," + + "bucket_id TEXT," + + "bucket_display_name TEXT" + + ");"); + } + + static void insertImageData(SQLiteDatabase db) { + insertImageData(db, 0xD000, 0xB000, "name"); + } + + static void insertImageData(SQLiteDatabase db, long dateTaken, + int bucketId, String bucketName) { + db.execSQL("INSERT INTO images (title, mime_type, latitude, longitude, " + + "datetaken, date_added, date_modified, bucket_id, " + + "bucket_display_name, _data, orientation) " + + "VALUES ('IMG_0072', 'image/jpeg', 12, 34, " + + dateTaken + ", 1280395646, 1275934796, '" + bucketId + "', " + + "'" + bucketName + "', " + + "'/mnt/sdcard/DCIM/100CANON/IMG_0072.JPG', 0)"); + } + + class TestZeroVideo extends TestLocalVideoAlbum { + @Override + public void prepareData(SQLiteDatabase db) { + createVideoTable(db); + } + + @Override + public void verifyResult() { + assertEquals(0, mAlbumSet.getMediaItemCount()); + assertEquals(0, mAlbumSet.getSubMediaSetCount()); + assertEquals(0, mAlbumSet.getTotalMediaItemCount()); + } + } + + class TestOneVideo extends TestLocalVideoAlbum { + @Override + public void prepareData(SQLiteDatabase db) { + createVideoTable(db); + insertVideoData(db); + } + + @Override + public void verifyResult() { + assertEquals(0, mAlbumSet.getMediaItemCount()); + assertEquals(1, mAlbumSet.getSubMediaSetCount()); + assertEquals(1, mAlbumSet.getTotalMediaItemCount()); + MediaSet sub = mAlbumSet.getSubMediaSet(0); + assertEquals(1, sub.getMediaItemCount()); + assertEquals(0, sub.getSubMediaSetCount()); + LocalMediaItem item = (LocalMediaItem) sub.getMediaItem(0, 1).get(0); + assertEquals(1, item.id); + assertEquals("VID_20100811_051413", item.caption); + assertEquals("video/mp4", item.mimeType); + assertEquals(11.0, item.latitude); + assertEquals(22.0, item.longitude); + assertEquals(0xD000, item.dateTakenInMs); + assertEquals(1281503663L, item.dateAddedInSec); + assertEquals(1281503662L, item.dateModifiedInSec); + assertEquals("/mnt/sdcard/DCIM/Camera/VID_20100811_051413.3gp", + item.filePath); + } + } + + class TestMoreVideos extends TestLocalVideoAlbum { + @Override + public void prepareData(SQLiteDatabase db) { + // Albums are sorted by names, and items are sorted by + // dateTimeTaken (descending) + createVideoTable(db); + // bucket 0xB002 + insertVideoData(db, 1000, 0xB000, "second"); // id 1 + insertVideoData(db, 2000, 0xB000, "second"); // id 2 + // bucket 0xB001 + insertVideoData(db, 3000, 0xB001, "first"); // id 3 + } + + @Override + public void verifyResult() { + assertEquals(0, mAlbumSet.getMediaItemCount()); + assertEquals(2, mAlbumSet.getSubMediaSetCount()); + assertEquals(3, mAlbumSet.getTotalMediaItemCount()); + + MediaSet first = mAlbumSet.getSubMediaSet(0); + assertEquals(1, first.getMediaItemCount()); + LocalMediaItem item = (LocalMediaItem) first.getMediaItem(0, 1).get(0); + assertEquals(3, item.id); + assertEquals(3000L, item.dateTakenInMs); + + MediaSet second = mAlbumSet.getSubMediaSet(1); + assertEquals(2, second.getMediaItemCount()); + item = (LocalMediaItem) second.getMediaItem(0, 1).get(0); + assertEquals(2, item.id); + assertEquals(2000L, item.dateTakenInMs); + item = (LocalMediaItem) second.getMediaItem(1, 1).get(0); + assertEquals(1, item.id); + assertEquals(1000L, item.dateTakenInMs); + } + } + + static void createVideoTable(SQLiteDatabase db) { + db.execSQL("CREATE TABLE IF NOT EXISTS video (" + + "_id INTEGER PRIMARY KEY," + + "_data TEXT NOT NULL," + + "_display_name TEXT," + + "_size INTEGER," + + "mime_type TEXT," + + "date_added INTEGER," + + "date_modified INTEGER," + + "title TEXT," + + "duration INTEGER," + + "artist TEXT," + + "album TEXT," + + "resolution TEXT," + + "description TEXT," + + "isprivate INTEGER," + // for YouTube videos + "tags TEXT," + // for YouTube videos + "category TEXT," + // for YouTube videos + "language TEXT," + // for YouTube videos + "mini_thumb_data TEXT," + + "latitude DOUBLE," + + "longitude DOUBLE," + + "datetaken INTEGER," + + "mini_thumb_magic INTEGER" + + ");"); + db.execSQL("ALTER TABLE video ADD COLUMN bucket_id TEXT;"); + db.execSQL("ALTER TABLE video ADD COLUMN bucket_display_name TEXT"); + } + + static void insertVideoData(SQLiteDatabase db) { + insertVideoData(db, 0xD000, 0xB000, "name"); + } + + static void insertVideoData(SQLiteDatabase db, long dateTaken, + int bucketId, String bucketName) { + db.execSQL("INSERT INTO video (title, mime_type, latitude, longitude, " + + "datetaken, date_added, date_modified, bucket_id, " + + "bucket_display_name, _data, duration) " + + "VALUES ('VID_20100811_051413', 'video/mp4', 11, 22, " + + dateTaken + ", 1281503663, 1281503662, '" + bucketId + "', " + + "'" + bucketName + "', " + + "'/mnt/sdcard/DCIM/Camera/VID_20100811_051413.3gp', 2964)"); + } + + static GalleryAppStub newGalleryContext(SQLiteDatabase db, Looper mainLooper) { + MockContentResolver cr = new MockContentResolver(); + ContentProvider cp = new DbContentProvider(db, cr); + cr.addProvider("media", cp); + return new GalleryAppMock(null, cr, mainLooper); + } +} + +class DbContentProvider extends MockContentProvider { + private static final String TAG = "DbContentProvider"; + private SQLiteDatabase mDatabase; + private ContentResolver mContentResolver; + + DbContentProvider(SQLiteDatabase db, ContentResolver cr) { + mDatabase = db; + mContentResolver = cr; + } + + @Override + public Cursor query(Uri uri, String[] projection, + String selection, String[] selectionArgs, String sortOrder) { + // This is a simplified version extracted from MediaProvider. + + String tableName = getTableName(uri); + if (tableName == null) return null; + + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + qb.setTables(tableName); + + String groupBy = null; + String limit = uri.getQueryParameter("limit"); + + if (uri.getQueryParameter("distinct") != null) { + qb.setDistinct(true); + } + + Log.v(TAG, "query = " + qb.buildQuery(projection, selection, + selectionArgs, groupBy, null, sortOrder, limit)); + + if (selectionArgs != null) { + for (String s : selectionArgs) { + Log.v(TAG, " selectionArgs = " + s); + } + } + + Cursor c = qb.query(mDatabase, projection, selection, + selectionArgs, groupBy, null, sortOrder, limit); + + return c; + } + + @Override + public int delete(Uri uri, String whereClause, String[] whereArgs) { + Log.v(TAG, "delete " + uri + "," + whereClause + "," + whereArgs[0]); + String tableName = getTableName(uri); + if (tableName == null) return 0; + int count = mDatabase.delete(tableName, whereClause, whereArgs); + mContentResolver.notifyChange(uri, null); + return count; + } + + private String getTableName(Uri uri) { + String uriString = uri.toString(); + if (uriString.startsWith("content://media/external/images/media")) { + return "images"; + } else if (uriString.startsWith("content://media/external/video/media")) { + return "video"; + } else { + return null; + } + } +} diff --git a/tests/src/com/android/gallery3d/data/MediaSetTest.java b/tests/src/com/android/gallery3d/data/MediaSetTest.java new file mode 100644 index 000000000..33dfe96de --- /dev/null +++ b/tests/src/com/android/gallery3d/data/MediaSetTest.java @@ -0,0 +1,63 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +public class MediaSetTest extends AndroidTestCase { + @SuppressWarnings("unused") + private static final String TAG = "MediaSetTest"; + + @SmallTest + public void testComboAlbumSet() { + GalleryApp app = new GalleryAppMock(null, null, null); + Path.clearAll(); + DataManager dataManager = app.getDataManager(); + + dataManager.addSource(new ComboSource(app)); + dataManager.addSource(new MockSource(app)); + + MockSet set00 = new MockSet(Path.fromString("/mock/00"), dataManager, 0, 2000); + MockSet set01 = new MockSet(Path.fromString("/mock/01"), dataManager, 1, 3000); + MockSet set10 = new MockSet(Path.fromString("/mock/10"), dataManager, 2, 4000); + MockSet set11 = new MockSet(Path.fromString("/mock/11"), dataManager, 3, 5000); + MockSet set12 = new MockSet(Path.fromString("/mock/12"), dataManager, 4, 6000); + + MockSet set0 = new MockSet(Path.fromString("/mock/0"), dataManager, 7, 7000); + set0.addMediaSet(set00); + set0.addMediaSet(set01); + + MockSet set1 = new MockSet(Path.fromString("/mock/1"), dataManager, 8, 8000); + set1.addMediaSet(set10); + set1.addMediaSet(set11); + set1.addMediaSet(set12); + + MediaSet combo = dataManager.getMediaSet("/combo/{/mock/0,/mock/1}"); + assertEquals(5, combo.getSubMediaSetCount()); + assertEquals(0, combo.getMediaItemCount()); + assertEquals("/mock/00", combo.getSubMediaSet(0).getPath().toString()); + assertEquals("/mock/01", combo.getSubMediaSet(1).getPath().toString()); + assertEquals("/mock/10", combo.getSubMediaSet(2).getPath().toString()); + assertEquals("/mock/11", combo.getSubMediaSet(3).getPath().toString()); + assertEquals("/mock/12", combo.getSubMediaSet(4).getPath().toString()); + + assertEquals(10, combo.getTotalMediaItemCount()); + } +} diff --git a/tests/src/com/android/gallery3d/data/MockItem.java b/tests/src/com/android/gallery3d/data/MockItem.java new file mode 100644 index 000000000..bd6dcd9cb --- /dev/null +++ b/tests/src/com/android/gallery3d/data/MockItem.java @@ -0,0 +1,43 @@ +/* + * 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.data; + +import com.android.gallery3d.util.ThreadPool.Job; + +import android.graphics.Bitmap; +import android.graphics.BitmapRegionDecoder; + +public class MockItem extends MediaItem { + public MockItem(Path path) { + super(path, nextVersionNumber()); + } + + @Override + public Job<Bitmap> requestImage(int type) { + return null; + } + + @Override + public Job<BitmapRegionDecoder> requestLargeImage() { + return null; + } + + @Override + public String getMimeType() { + return null; + } +} diff --git a/tests/src/com/android/gallery3d/data/MockSet.java b/tests/src/com/android/gallery3d/data/MockSet.java new file mode 100644 index 000000000..fa83c796f --- /dev/null +++ b/tests/src/com/android/gallery3d/data/MockSet.java @@ -0,0 +1,88 @@ +/* + * 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.data; + +import java.util.ArrayList; + +public class MockSet extends MediaSet { + ArrayList<MediaItem> mItems = new ArrayList<MediaItem>(); + ArrayList<MediaSet> mSets = new ArrayList<MediaSet>(); + Path mItemPath; + + public MockSet(Path path, DataManager dataManager) { + super(path, nextVersionNumber()); + mItemPath = Path.fromString("/mock/item"); + } + + public MockSet(Path path, DataManager dataManager, + int items, int item_id_start) { + this(path, dataManager); + for (int i = 0; i < items; i++) { + Path childPath = mItemPath.getChild(item_id_start + i); + mItems.add(new MockItem(childPath)); + } + } + + public void addMediaSet(MediaSet sub) { + mSets.add(sub); + } + + @Override + public int getMediaItemCount() { + return mItems.size(); + } + + @Override + public ArrayList<MediaItem> getMediaItem(int start, int count) { + ArrayList<MediaItem> result = new ArrayList<MediaItem>(); + int end = Math.min(start + count, mItems.size()); + + for (int i = start; i < end; i++) { + result.add(mItems.get(i)); + } + return result; + } + + @Override + public int getSubMediaSetCount() { + return mSets.size(); + } + + @Override + public MediaSet getSubMediaSet(int index) { + return mSets.get(index); + } + + @Override + public int getTotalMediaItemCount() { + int result = mItems.size(); + for (MediaSet s : mSets) { + result += s.getTotalMediaItemCount(); + } + return result; + } + + @Override + public String getName() { + return "Set " + mPath; + } + + @Override + public long reload() { + return 0; + } +} diff --git a/tests/src/com/android/gallery3d/data/MockSource.java b/tests/src/com/android/gallery3d/data/MockSource.java new file mode 100644 index 000000000..27ed4d0de --- /dev/null +++ b/tests/src/com/android/gallery3d/data/MockSource.java @@ -0,0 +1,48 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; + +class MockSource extends MediaSource { + GalleryApp mApplication; + PathMatcher mMatcher; + + private static final int MOCK_SET = 0; + private static final int MOCK_ITEM = 1; + + public MockSource(GalleryApp context) { + super("mock"); + mApplication = context; + mMatcher = new PathMatcher(); + mMatcher.add("/mock/*", MOCK_SET); + mMatcher.add("/mock/item/*", MOCK_ITEM); + } + + @Override + public MediaObject createMediaObject(Path path) { + MediaObject obj; + switch (mMatcher.match(path)) { + case MOCK_SET: + return new MockSet(path, mApplication.getDataManager()); + case MOCK_ITEM: + return new MockItem(path); + default: + throw new RuntimeException("bad path: " + path); + } + } +} diff --git a/tests/src/com/android/gallery3d/data/PathTest.java b/tests/src/com/android/gallery3d/data/PathTest.java new file mode 100644 index 000000000..b43d10963 --- /dev/null +++ b/tests/src/com/android/gallery3d/data/PathTest.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.data; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +public class PathTest extends AndroidTestCase { + @SuppressWarnings("unused") + private static final String TAG = "PathTest"; + + @SmallTest + public void testToString() { + Path p = Path.fromString("/hello/world"); + assertEquals("/hello/world", p.toString()); + + p = Path.fromString("/a"); + assertEquals("/a", p.toString()); + + p = Path.fromString(""); + assertEquals("", p.toString()); + } + + @SmallTest + public void testSplit() { + Path p = Path.fromString("/hello/world"); + String[] s = p.split(); + assertEquals(2, s.length); + assertEquals("hello", s[0]); + assertEquals("world", s[1]); + + p = Path.fromString(""); + assertEquals(0, p.split().length); + } + + @SmallTest + public void testPrefix() { + Path p = Path.fromString("/hello/world"); + assertEquals("hello", p.getPrefix()); + + p = Path.fromString(""); + assertEquals("", p.getPrefix()); + } + + @SmallTest + public void testGetChild() { + Path p = Path.fromString("/hello"); + Path q = Path.fromString("/hello/world"); + assertSame(q, p.getChild("world")); + Path r = q.getChild(17); + assertEquals("/hello/world/17", r.toString()); + } + + @SmallTest + public void testSplitSequence() { + String[] s = Path.splitSequence("{a,bb,ccc}"); + assertEquals(3, s.length); + assertEquals("a", s[0]); + assertEquals("bb", s[1]); + assertEquals("ccc", s[2]); + + s = Path.splitSequence("{a,{bb,ccc},d}"); + assertEquals(3, s.length); + assertEquals("a", s[0]); + assertEquals("{bb,ccc}", s[1]); + assertEquals("d", s[2]); + } +} diff --git a/tests/src/com/android/gallery3d/data/RealDataTest.java b/tests/src/com/android/gallery3d/data/RealDataTest.java new file mode 100644 index 000000000..526cfe357 --- /dev/null +++ b/tests/src/com/android/gallery3d/data/RealDataTest.java @@ -0,0 +1,110 @@ +/* + * 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.data; + +import com.android.gallery3d.app.GalleryApp; +import com.android.gallery3d.picasasource.PicasaSource; + +import android.os.Looper; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.LargeTest; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashSet; + +// This test reads real data directly and dump information out in the log. +public class RealDataTest extends AndroidTestCase { + private static final String TAG = "RealDataTest"; + + private HashSet<Path> mUsedId = new HashSet<Path>(); + private GalleryApp mApplication; + private DataManager mDataManager; + + @LargeTest + public void testRealData() { + mUsedId.clear(); + mApplication = new GalleryAppMock( + mContext, + mContext.getContentResolver(), + Looper.myLooper()); + mDataManager = mApplication.getDataManager(); + mDataManager.addSource(new LocalSource(mApplication)); + mDataManager.addSource(new PicasaSource(mApplication)); + new TestLocalImage().run(); + new TestLocalVideo().run(); + new TestPicasa().run(); + } + + class TestLocalImage { + public void run() { + MediaSet set = mDataManager.getMediaSet("/local/image"); + set.reload(); + Log.v(TAG, "LocalAlbumSet (Image)"); + dumpMediaSet(set, ""); + } + } + + class TestLocalVideo { + public void run() { + MediaSet set = mDataManager.getMediaSet("/local/video"); + set.reload(); + Log.v(TAG, "LocalAlbumSet (Video)"); + dumpMediaSet(set, ""); + } + } + + class TestPicasa implements Runnable { + public void run() { + MediaSet set = mDataManager.getMediaSet("/picasa"); + set.reload(); + Log.v(TAG, "PicasaAlbumSet"); + dumpMediaSet(set, ""); + } + } + + void dumpMediaSet(MediaSet set, String prefix) { + Log.v(TAG, "getName() = " + set.getName()); + Log.v(TAG, "getPath() = " + set.getPath()); + Log.v(TAG, "getMediaItemCount() = " + set.getMediaItemCount()); + Log.v(TAG, "getSubMediaSetCount() = " + set.getSubMediaSetCount()); + Log.v(TAG, "getTotalMediaItemCount() = " + set.getTotalMediaItemCount()); + assertNewId(set.getPath()); + for (int i = 0, n = set.getSubMediaSetCount(); i < n; i++) { + MediaSet sub = set.getSubMediaSet(i); + Log.v(TAG, prefix + "got set " + i); + dumpMediaSet(sub, prefix + " "); + } + for (int i = 0, n = set.getMediaItemCount(); i < n; i += 10) { + ArrayList<MediaItem> list = set.getMediaItem(i, 10); + Log.v(TAG, prefix + "got item " + i + " (+" + list.size() + ")"); + for (MediaItem item : list) { + dumpMediaItem(item, prefix + ".."); + } + } + } + + void dumpMediaItem(MediaItem item, String prefix) { + assertNewId(item.getPath()); + Log.v(TAG, prefix + "getPath() = " + item.getPath()); + } + + void assertNewId(Path key) { + assertFalse(key + " has already appeared.", mUsedId.contains(key)); + mUsedId.add(key); + } +} diff --git a/tests/src/com/android/gallery3d/ui/GLCanvasMock.java b/tests/src/com/android/gallery3d/ui/GLCanvasMock.java new file mode 100644 index 000000000..f8100ddf6 --- /dev/null +++ b/tests/src/com/android/gallery3d/ui/GLCanvasMock.java @@ -0,0 +1,68 @@ +/* + * 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.ui; + +import javax.microedition.khronos.opengles.GL11; + +public class GLCanvasMock extends GLCanvasStub { + // fillRect + int mFillRectCalled; + float mFillRectWidth; + float mFillRectHeight; + int mFillRectColor; + // drawMixed + int mDrawMixedCalled; + float mDrawMixedRatio; + // drawTexture; + int mDrawTextureCalled; + + private GL11 mGL; + + public GLCanvasMock(GL11 gl) { + mGL = gl; + } + + public GLCanvasMock() { + mGL = new GLStub(); + } + + @Override + public GL11 getGLInstance() { + return mGL; + } + + @Override + public void fillRect(float x, float y, float width, float height, int color) { + mFillRectCalled++; + mFillRectWidth = width; + mFillRectHeight = height; + mFillRectColor = color; + } + + @Override + public void drawTexture( + BasicTexture texture, int x, int y, int width, int height) { + mDrawTextureCalled++; + } + + @Override + public void drawMixed(BasicTexture from, BasicTexture to, + float ratio, int x, int y, int w, int h) { + mDrawMixedCalled++; + mDrawMixedRatio = ratio; + } +} diff --git a/tests/src/com/android/gallery3d/ui/GLCanvasStub.java b/tests/src/com/android/gallery3d/ui/GLCanvasStub.java new file mode 100644 index 000000000..f1663f4bd --- /dev/null +++ b/tests/src/com/android/gallery3d/ui/GLCanvasStub.java @@ -0,0 +1,79 @@ +/* + * 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.ui; + +import android.graphics.RectF; + +import javax.microedition.khronos.opengles.GL11; + +public class GLCanvasStub implements GLCanvas { + public void setSize(int width, int height) {} + public void clearBuffer() {} + public void setCurrentAnimationTimeMillis(long time) {} + public long currentAnimationTimeMillis() { + throw new UnsupportedOperationException(); + } + public void setAlpha(float alpha) {} + public float getAlpha() { + throw new UnsupportedOperationException(); + } + public void multiplyAlpha(float alpha) {} + public void translate(float x, float y, float z) {} + public void scale(float sx, float sy, float sz) {} + public void rotate(float angle, float x, float y, float z) {} + public boolean clipRect(int left, int top, int right, int bottom) { + throw new UnsupportedOperationException(); + } + public int save() { + throw new UnsupportedOperationException(); + } + public int save(int saveFlags) { + throw new UnsupportedOperationException(); + } + public void setBlendEnabled(boolean enabled) {} + public void restore() {} + public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) {} + public void drawRect(float x1, float y1, float x2, float y2, GLPaint paint) {} + public void fillRect(float x, float y, float width, float height, int color) {} + public void drawTexture( + BasicTexture texture, int x, int y, int width, int height) {} + public void drawMesh(BasicTexture tex, int x, int y, int xyBuffer, + int uvBuffer, int indexBuffer, int indexCount) {} + public void drawTexture(BasicTexture texture, + int x, int y, int width, int height, float alpha) {} + public void drawTexture(BasicTexture texture, RectF source, RectF target) {} + public void drawMixed(BasicTexture from, BasicTexture to, + float ratio, int x, int y, int w, int h) {} + public void drawMixed(BasicTexture from, int to, + float ratio, int x, int y, int w, int h) {} + public void drawMixed(BasicTexture from, BasicTexture to, + float ratio, int x, int y, int width, int height, float alpha) {} + public BasicTexture copyTexture(int x, int y, int width, int height) { + throw new UnsupportedOperationException(); + } + public GL11 getGLInstance() { + throw new UnsupportedOperationException(); + } + public boolean unloadTexture(BasicTexture texture) { + throw new UnsupportedOperationException(); + } + public void deleteBuffer(int bufferId) { + throw new UnsupportedOperationException(); + } + public void deleteRecycledResources() {} + public void multiplyMatrix(float[] mMatrix, int offset) {} +} diff --git a/tests/src/com/android/gallery3d/ui/GLCanvasTest.java b/tests/src/com/android/gallery3d/ui/GLCanvasTest.java new file mode 100644 index 000000000..528b04f67 --- /dev/null +++ b/tests/src/com/android/gallery3d/ui/GLCanvasTest.java @@ -0,0 +1,778 @@ +/* + * 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.ui; + +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; + +import junit.framework.TestCase; + +import java.util.Arrays; + +import javax.microedition.khronos.opengles.GL10; +import javax.microedition.khronos.opengles.GL11; + +@SmallTest +public class GLCanvasTest extends TestCase { + private static final String TAG = "GLCanvasTest"; + + private static GLPaint newColorPaint(int color) { + GLPaint paint = new GLPaint(); + paint.setColor(color); + return paint; + } + + @SmallTest + public void testSetSize() { + GL11 glStub = new GLStub(); + GLCanvas canvas = new GLCanvasImpl(glStub); + canvas.setSize(100, 200); + canvas.setSize(1000, 100); + try { + canvas.setSize(-1, 100); + fail(); + } catch (Throwable ex) { + // expected. + } + } + + @SmallTest + public void testClearBuffer() { + new ClearBufferTest().run(); + } + + private static class ClearBufferTest extends GLMock { + void run() { + GLCanvas canvas = new GLCanvasImpl(this); + assertEquals(0, mGLClearCalled); + canvas.clearBuffer(); + assertEquals(GL10.GL_COLOR_BUFFER_BIT, mGLClearMask); + assertEquals(1, mGLClearCalled); + } + } + + @SmallTest + public void testAnimationTime() { + GL11 glStub = new GLStub(); + GLCanvas canvas = new GLCanvasImpl(glStub); + + long[] testData = {0, 1, 2, 1000, 10000, Long.MAX_VALUE}; + + for (long v : testData) { + canvas.setCurrentAnimationTimeMillis(v); + assertEquals(v, canvas.currentAnimationTimeMillis()); + } + + try { + canvas.setCurrentAnimationTimeMillis(-1); + fail(); + } catch (Throwable ex) { + // expected. + } + } + + @SmallTest + public void testSetColor() { + new SetColorTest().run(); + } + + // This test assumes we use pre-multipled alpha blending and should + // set the blending function and color correctly. + private static class SetColorTest extends GLMock { + void run() { + int[] testColors = new int[] { + 0, 0xFFFFFFFF, 0xFF000000, 0x00FFFFFF, 0x80FF8001, + 0x7F010101, 0xFEFEFDFC, 0x017F8081, 0x027F8081, 0x2ADE4C4D + }; + + GLCanvas canvas = new GLCanvasImpl(this); + canvas.setSize(400, 300); + // Test one color to make sure blend function is set. + assertEquals(0, mGLColorCalled); + canvas.drawLine(0, 0, 1, 1, newColorPaint(0x7F804020)); + assertEquals(1, mGLColorCalled); + assertEquals(0x7F402010, mGLColor); + assertPremultipliedBlending(this); + + // Test other colors to make sure premultiplication is right + for (int c : testColors) { + float a = (c >>> 24) / 255f; + float r = ((c >> 16) & 0xff) / 255f; + float g = ((c >> 8) & 0xff) / 255f; + float b = (c & 0xff) / 255f; + int pre = makeColor4f(a * r, a * g, a * b, a); + + mGLColorCalled = 0; + canvas.drawLine(0, 0, 1, 1, newColorPaint(c)); + assertEquals(1, mGLColorCalled); + assertEquals(pre, mGLColor); + } + } + } + + @SmallTest + public void testSetGetMultiplyAlpha() { + GL11 glStub = new GLStub(); + GLCanvas canvas = new GLCanvasImpl(glStub); + + canvas.setAlpha(1f); + assertEquals(1f, canvas.getAlpha()); + + canvas.setAlpha(0f); + assertEquals(0f, canvas.getAlpha()); + + canvas.setAlpha(0.5f); + assertEquals(0.5f, canvas.getAlpha()); + + canvas.multiplyAlpha(0.5f); + assertEquals(0.25f, canvas.getAlpha()); + + canvas.multiplyAlpha(0f); + assertEquals(0f, canvas.getAlpha()); + + try { + canvas.setAlpha(-0.01f); + fail(); + } catch (Throwable ex) { + // expected. + } + + try { + canvas.setAlpha(1.01f); + fail(); + } catch (Throwable ex) { + // expected. + } + } + + @SmallTest + public void testAlpha() { + new AlphaTest().run(); + } + + private static class AlphaTest extends GLMock { + void run() { + GLCanvas canvas = new GLCanvasImpl(this); + canvas.setSize(400, 300); + + assertEquals(0, mGLColorCalled); + canvas.setAlpha(0.48f); + canvas.drawLine(0, 0, 1, 1, newColorPaint(0xFF804020)); + assertPremultipliedBlending(this); + assertEquals(1, mGLColorCalled); + assertEquals(0x7A3D1F0F, mGLColor); + } + } + + @SmallTest + public void testDrawLine() { + new DrawLineTest().run(); + } + + // This test assumes the drawLine() function use glDrawArrays() with + // GL_LINE_STRIP mode to draw the line and the input coordinates are used + // directly. + private static class DrawLineTest extends GLMock { + private int mDrawArrayCalled = 0; + private final int[] mResult = new int[4]; + + @Override + public void glDrawArrays(int mode, int first, int count) { + assertNotNull(mGLVertexPointer); + assertEquals(GL10.GL_LINE_STRIP, mode); + assertEquals(2, count); + mGLVertexPointer.bindByteBuffer(); + + double[] coord = new double[4]; + mGLVertexPointer.getArrayElement(first, coord); + mResult[0] = (int) coord[0]; + mResult[1] = (int) coord[1]; + mGLVertexPointer.getArrayElement(first + 1, coord); + mResult[2] = (int) coord[0]; + mResult[3] = (int) coord[1]; + mDrawArrayCalled++; + } + + void run() { + GLCanvas canvas = new GLCanvasImpl(this); + canvas.setSize(400, 300); + canvas.drawLine(2, 7, 1, 8, newColorPaint(0) /* color */); + assertTrue(mGLVertexArrayEnabled); + assertEquals(1, mDrawArrayCalled); + + Log.v(TAG, "result = " + Arrays.toString(mResult)); + int[] answer = new int[] {2, 7, 1, 8}; + for (int i = 0; i < answer.length; i++) { + assertEquals(answer[i], mResult[i]); + } + } + } + + @SmallTest + public void testFillRect() { + new FillRectTest().run(); + } + + // This test assumes the drawLine() function use glDrawArrays() with + // GL_TRIANGLE_STRIP mode to draw the line and the input coordinates + // are used directly. + private static class FillRectTest extends GLMock { + private int mDrawArrayCalled = 0; + private final int[] mResult = new int[8]; + + @Override + public void glDrawArrays(int mode, int first, int count) { + assertNotNull(mGLVertexPointer); + assertEquals(GL10.GL_TRIANGLE_STRIP, mode); + assertEquals(4, count); + mGLVertexPointer.bindByteBuffer(); + + double[] coord = new double[4]; + for (int i = 0; i < 4; i++) { + mGLVertexPointer.getArrayElement(first + i, coord); + mResult[i * 2 + 0] = (int) coord[0]; + mResult[i * 2 + 1] = (int) coord[1]; + } + + mDrawArrayCalled++; + } + + void run() { + GLCanvas canvas = new GLCanvasImpl(this); + canvas.setSize(400, 300); + canvas.fillRect(2, 7, 1, 8, 0 /* color */); + assertTrue(mGLVertexArrayEnabled); + assertEquals(1, mDrawArrayCalled); + Log.v(TAG, "result = " + Arrays.toString(mResult)); + + // These are the four vertics that should be used. + int[] answer = new int[] { + 2, 7, + 3, 7, + 3, 15, + 2, 15}; + int count[] = new int[4]; + + // Count the number of appearances for each vertex. + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + if (answer[i * 2] == mResult[j * 2] && + answer[i * 2 + 1] == mResult[j * 2 + 1]) { + count[i]++; + } + } + } + + // Each vertex should appear exactly once. + for (int i = 0; i < 4; i++) { + assertEquals(1, count[i]); + } + } + } + + @SmallTest + public void testTransform() { + new TransformTest().run(); + } + + // This test assumes glLoadMatrixf is used to load the model view matrix, + // and glOrthof is used to load the projection matrix. + // + // The projection matrix is set to an orthogonal projection which is the + // inverse of viewport transform. So the model view matrix maps input + // directly to screen coordinates (default no scaling, and the y-axis is + // reversed). + // + // The matrix here are all listed in column major order. + // + private static class TransformTest extends GLMock { + private final float[] mModelViewMatrixUsed = new float[16]; + private final float[] mProjectionMatrixUsed = new float[16]; + + @Override + public void glDrawArrays(int mode, int first, int count) { + copy(mModelViewMatrixUsed, mGLModelViewMatrix); + copy(mProjectionMatrixUsed, mGLProjectionMatrix); + } + + private void copy(float[] dest, float[] src) { + System.arraycopy(src, 0, dest, 0, 16); + } + + void run() { + GLCanvas canvas = new GLCanvasImpl(this); + canvas.setSize(40, 50); + int color = 0; + + // Initial matrix + canvas.drawLine(0, 0, 1, 1, newColorPaint(color)); + assertMatrixEq(new float[] { + 1, 0, 0, 0, + 0, -1, 0, 0, + 0, 0, 1, 0, + 0, 50, 0, 1 + }, mModelViewMatrixUsed); + + assertMatrixEq(new float[] { + 2f / 40, 0, 0, 0, + 0, 2f / 50, 0, 0, + 0, 0, -1, 0, + -1, -1, 0, 1 + }, mProjectionMatrixUsed); + + // Translation + canvas.translate(3, 4, 5); + canvas.drawLine(0, 0, 1, 1, newColorPaint(color)); + assertMatrixEq(new float[] { + 1, 0, 0, 0, + 0, -1, 0, 0, + 0, 0, 1, 0, + 3, 46, 5, 1 + }, mModelViewMatrixUsed); + canvas.save(); + + // Scaling + canvas.scale(0.7f, 0.6f, 0.5f); + canvas.drawLine(0, 0, 1, 1, newColorPaint(color)); + assertMatrixEq(new float[] { + 0.7f, 0, 0, 0, + 0, -0.6f, 0, 0, + 0, 0, 0.5f, 0, + 3, 46, 5, 1 + }, mModelViewMatrixUsed); + + // Rotation + canvas.rotate(90, 0, 0, 1); + canvas.drawLine(0, 0, 1, 1, newColorPaint(color)); + assertMatrixEq(new float[] { + 0, -0.6f, 0, 0, + -0.7f, 0, 0, 0, + 0, 0, 0.5f, 0, + 3, 46, 5, 1 + }, mModelViewMatrixUsed); + canvas.restore(); + + // After restoring to the point just after translation, + // do rotation again. + canvas.rotate(180, 1, 0, 0); + canvas.drawLine(0, 0, 1, 1, newColorPaint(color)); + assertMatrixEq(new float[] { + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, -1, 0, + 3, 46, 5, 1 + }, mModelViewMatrixUsed); + } + } + + @SmallTest + public void testClipRect() { + // The test is currently broken, waiting for the fix + // new ClipRectTest().run(); + } + + private static class ClipRectTest extends GLStub { + int mX, mY, mWidth, mHeight; + + @Override + public void glScissor(int x, int y, int width, int height) { + mX = x; + mY = 100 - y - height; // flip in Y direction + mWidth = width; + mHeight = height; + } + + private void assertClipRect(int x, int y, int width, int height) { + assertEquals(x, mX); + assertEquals(y, mY); + assertEquals(width, mWidth); + assertEquals(height, mHeight); + } + + private void assertEmptyClipRect() { + assertEquals(0, mWidth); + assertEquals(0, mHeight); + } + + void run() { + GLCanvas canvas = new GLCanvasImpl(this); + canvas.setSize(100, 100); + canvas.save(); + assertClipRect(0, 0, 100, 100); + + assertTrue(canvas.clipRect(10, 10, 70, 70)); + canvas.save(); + assertClipRect(10, 10, 60, 60); + + assertTrue(canvas.clipRect(30, 30, 90, 90)); + canvas.save(); + assertClipRect(30, 30, 40, 40); + + assertTrue(canvas.clipRect(40, 40, 60, 90)); + assertClipRect(40, 40, 20, 30); + + assertFalse(canvas.clipRect(30, 30, 70, 40)); + assertEmptyClipRect(); + assertFalse(canvas.clipRect(0, 0, 100, 100)); + assertEmptyClipRect(); + + canvas.restore(); + assertClipRect(30, 30, 40, 40); + + canvas.restore(); + assertClipRect(10, 10, 60, 60); + + canvas.restore(); + assertClipRect(0, 0, 100, 100); + + canvas.translate(10, 20, 30); + assertTrue(canvas.clipRect(10, 10, 70, 70)); + canvas.save(); + assertClipRect(20, 30, 60, 60); + } + } + + @SmallTest + public void testSaveRestore() { + new SaveRestoreTest().run(); + } + + private static class SaveRestoreTest extends GLStub { + int mX, mY, mWidth, mHeight; + + @Override + public void glScissor(int x, int y, int width, int height) { + mX = x; + mY = 100 - y - height; // flip in Y direction + mWidth = width; + mHeight = height; + } + + private void assertClipRect(int x, int y, int width, int height) { + assertEquals(x, mX); + assertEquals(y, mY); + assertEquals(width, mWidth); + assertEquals(height, mHeight); + } + + void run() { + GLCanvas canvas = new GLCanvasImpl(this); + canvas.setSize(100, 100); + + canvas.setAlpha(0.7f); + assertTrue(canvas.clipRect(10, 10, 70, 70)); + + canvas.save(canvas.SAVE_FLAG_CLIP); + canvas.setAlpha(0.6f); + assertTrue(canvas.clipRect(30, 30, 90, 90)); + + canvas.save(canvas.SAVE_FLAG_CLIP | canvas.SAVE_FLAG_ALPHA); + canvas.setAlpha(0.5f); + assertTrue(canvas.clipRect(40, 40, 60, 90)); + + assertEquals(0.5f, canvas.getAlpha()); + assertClipRect(40, 40, 20, 30); + + canvas.restore(); // now both clipping rect and alpha are restored. + assertEquals(0.6f, canvas.getAlpha()); + assertClipRect(30, 30, 40, 40); + + canvas.restore(); // now only clipping rect is restored. + + canvas.save(0); + canvas.save(0); + canvas.restore(); + canvas.restore(); + + assertEquals(0.6f, canvas.getAlpha()); + assertTrue(canvas.clipRect(10, 10, 60, 60)); + } + } + + @SmallTest + public void testDrawTexture() { + new DrawTextureTest().run(); + new DrawTextureMixedTest().run(); + } + + private static class MyTexture extends BasicTexture { + boolean mIsOpaque; + int mBindCalled; + + MyTexture(GLCanvas canvas, int id, boolean isOpaque) { + super(canvas, id, STATE_LOADED); + setSize(1, 1); + mIsOpaque = isOpaque; + } + + @Override + protected boolean onBind(GLCanvas canvas) { + mBindCalled++; + return true; + } + + public boolean isOpaque() { + return mIsOpaque; + } + } + + private static class DrawTextureTest extends GLMock { + int mDrawTexiOESCalled; + int mDrawArrayCalled; + int[] mResult = new int[4]; + + @Override + public void glDrawTexiOES(int x, int y, int z, + int width, int height) { + mDrawTexiOESCalled++; + } + + @Override + public void glDrawArrays(int mode, int first, int count) { + assertNotNull(mGLVertexPointer); + assertEquals(GL10.GL_TRIANGLE_STRIP, mode); + assertEquals(4, count); + mGLVertexPointer.bindByteBuffer(); + + double[] coord = new double[4]; + mGLVertexPointer.getArrayElement(first, coord); + mResult[0] = (int) coord[0]; + mResult[1] = (int) coord[1]; + mGLVertexPointer.getArrayElement(first + 1, coord); + mResult[2] = (int) coord[0]; + mResult[3] = (int) coord[1]; + mDrawArrayCalled++; + } + + void run() { + GLCanvas canvas = new GLCanvasImpl(this); + canvas.setSize(400, 300); + MyTexture texture = new MyTexture(canvas, 42, false); // non-opaque + MyTexture texture_o = new MyTexture(canvas, 47, true); // opaque + + // Draw a non-opaque texture + canvas.drawTexture(texture, 100, 200, 300, 400); + assertEquals(42, mGLBindTextureId); + assertEquals(GL_REPLACE, getTexEnvi(GL_TEXTURE_ENV_MODE)); + assertPremultipliedBlending(this); + assertFalse(mGLStencilEnabled); + + // Draw an opaque texture + canvas.drawTexture(texture_o, 100, 200, 300, 400); + assertEquals(47, mGLBindTextureId); + assertEquals(GL_REPLACE, getTexEnvi(GL_TEXTURE_ENV_MODE)); + assertFalse(mGLBlendEnabled); + + // Draw a non-opaque texture with alpha = 0.5 + canvas.setAlpha(0.5f); + canvas.drawTexture(texture, 100, 200, 300, 400); + assertEquals(42, mGLBindTextureId); + assertEquals(0x80808080, mGLColor); + assertEquals(GL_MODULATE, getTexEnvi(GL_TEXTURE_ENV_MODE)); + assertPremultipliedBlending(this); + assertFalse(mGLStencilEnabled); + + // Draw an non-opaque texture with overriden alpha = 1 + canvas.drawTexture(texture, 100, 200, 300, 400, 1f); + assertEquals(42, mGLBindTextureId); + assertEquals(GL_REPLACE, getTexEnvi(GL_TEXTURE_ENV_MODE)); + assertPremultipliedBlending(this); + + // Draw an opaque texture with overriden alpha = 1 + canvas.drawTexture(texture_o, 100, 200, 300, 400, 1f); + assertEquals(47, mGLBindTextureId); + assertEquals(GL_REPLACE, getTexEnvi(GL_TEXTURE_ENV_MODE)); + assertFalse(mGLBlendEnabled); + + // Draw an opaque texture with overridden alpha = 0.25 + canvas.drawTexture(texture_o, 100, 200, 300, 400, 0.25f); + assertEquals(47, mGLBindTextureId); + assertEquals(0x40404040, mGLColor); + assertEquals(GL_MODULATE, getTexEnvi(GL_TEXTURE_ENV_MODE)); + assertPremultipliedBlending(this); + + // Draw an opaque texture with overridden alpha = 0.125 + // but with some rotation so it will use DrawArray. + canvas.save(); + canvas.rotate(30, 0, 0, 1); + canvas.drawTexture(texture_o, 100, 200, 300, 400, 0.125f); + canvas.restore(); + assertEquals(47, mGLBindTextureId); + assertEquals(0x20202020, mGLColor); + assertEquals(GL_MODULATE, getTexEnvi(GL_TEXTURE_ENV_MODE)); + assertPremultipliedBlending(this); + + // We have drawn seven textures above. + assertEquals(1, mDrawArrayCalled); + assertEquals(6, mDrawTexiOESCalled); + + // translate and scale does not affect whether we + // can use glDrawTexiOES, but rotate may. + canvas.translate(10, 20, 30); + canvas.drawTexture(texture, 100, 200, 300, 400); + assertEquals(7, mDrawTexiOESCalled); + + canvas.scale(10, 20, 30); + canvas.drawTexture(texture, 100, 200, 300, 400); + assertEquals(8, mDrawTexiOESCalled); + + canvas.rotate(90, 1, 2, 3); + canvas.drawTexture(texture, 100, 200, 300, 400); + assertEquals(8, mDrawTexiOESCalled); + + canvas.rotate(-90, 1, 2, 3); + canvas.drawTexture(texture, 100, 200, 300, 400); + assertEquals(9, mDrawTexiOESCalled); + + canvas.rotate(180, 0, 0, 1); + canvas.drawTexture(texture, 100, 200, 300, 400); + assertEquals(9, mDrawTexiOESCalled); + + canvas.rotate(180, 0, 0, 1); + canvas.drawTexture(texture, 100, 200, 300, 400); + assertEquals(10, mDrawTexiOESCalled); + + assertEquals(3, mDrawArrayCalled); + + assertTrue(texture.isLoaded(canvas)); + texture.recycle(); + assertFalse(texture.isLoaded(canvas)); + canvas.deleteRecycledResources(); + + assertTrue(texture_o.isLoaded(canvas)); + texture_o.recycle(); + assertFalse(texture_o.isLoaded(canvas)); + } + } + + private static class DrawTextureMixedTest extends GLMock { + + boolean mTexture2DEnabled0, mTexture2DEnabled1; + int mBindTexture0; + int mBindTexture1; + + @Override + public void glEnable(int cap) { + if (cap == GL_TEXTURE_2D) { + texture2DEnable(true); + } + } + + @Override + public void glDisable(int cap) { + if (cap == GL_TEXTURE_2D) { + texture2DEnable(false); + } + } + + private void texture2DEnable(boolean enable) { + if (mGLActiveTexture == GL_TEXTURE0) { + mTexture2DEnabled0 = enable; + } else if (mGLActiveTexture == GL_TEXTURE1) { + mTexture2DEnabled1 = enable; + } else { + fail(); + } + } + + @Override + public void glTexEnvfv(int target, int pname, float[] params, int offset) { + if (target == GL_TEXTURE_ENV && pname == GL_TEXTURE_ENV_COLOR) { + assertEquals(0.5f, params[offset + 3]); + } + } + + @Override + public void glBindTexture(int target, int texture) { + if (target == GL_TEXTURE_2D) { + if (mGLActiveTexture == GL_TEXTURE0) { + mBindTexture0 = texture; + } else if (mGLActiveTexture == GL_TEXTURE1) { + mBindTexture1 = texture; + } else { + fail(); + } + } + } + + void run() { + GLCanvas canvas = new GLCanvasImpl(this); + canvas.setSize(400, 300); + MyTexture from = new MyTexture(canvas, 42, false); // non-opaque + MyTexture to = new MyTexture(canvas, 47, true); // opaque + + canvas.drawMixed(from, to, 0.5f, 100, 200, 300, 400); + assertEquals(42, mBindTexture0); + assertEquals(47, mBindTexture1); + assertTrue(mTexture2DEnabled0); + assertFalse(mTexture2DEnabled1); + + assertEquals(GL_COMBINE, getTexEnvi(GL_TEXTURE1, GL_TEXTURE_ENV_MODE)); + assertEquals(GL_INTERPOLATE, getTexEnvi(GL_TEXTURE1, GL_COMBINE_RGB)); + assertEquals(GL_INTERPOLATE, getTexEnvi(GL_TEXTURE1, GL_COMBINE_ALPHA)); + assertEquals(GL_CONSTANT, getTexEnvi(GL_TEXTURE1, GL_SRC2_RGB)); + assertEquals(GL_CONSTANT, getTexEnvi(GL_TEXTURE1, GL_SRC2_ALPHA)); + assertEquals(GL_SRC_ALPHA, getTexEnvi(GL_TEXTURE1, GL_OPERAND2_RGB)); + assertEquals(GL_SRC_ALPHA, getTexEnvi(GL_TEXTURE1, GL_OPERAND2_ALPHA)); + + assertEquals(GL_REPLACE, getTexEnvi(GL_TEXTURE0, GL_TEXTURE_ENV_MODE)); + + assertFalse(mGLBlendEnabled); + + canvas.drawMixed(from, to, 0, 100, 200, 300, 400); + assertEquals(GL_REPLACE, getTexEnvi(GL_TEXTURE0, GL_TEXTURE_ENV_MODE)); + assertEquals(42, mBindTexture0); + + canvas.drawMixed(from, to, 1, 100, 200, 300, 400); + assertEquals(GL_REPLACE, getTexEnvi(GL_TEXTURE0, GL_TEXTURE_ENV_MODE)); + assertEquals(47, mBindTexture0); + } + } + + @SmallTest + public void testGetGLInstance() { + GL11 glStub = new GLStub(); + GLCanvas canvas = new GLCanvasImpl(glStub); + assertSame(glStub, canvas.getGLInstance()); + } + + private static void assertPremultipliedBlending(GLMock mock) { + assertTrue(mock.mGLBlendFuncCalled > 0); + assertTrue(mock.mGLBlendEnabled); + assertEquals(GL11.GL_ONE, mock.mGLBlendFuncSFactor); + assertEquals(GL11.GL_ONE_MINUS_SRC_ALPHA, mock.mGLBlendFuncDFactor); + } + + private static void assertMatrixEq(float[] expected, float[] actual) { + try { + for (int i = 0; i < 16; i++) { + assertFloatEq(expected[i], actual[i]); + } + } catch (Throwable t) { + Log.v(TAG, "expected = " + Arrays.toString(expected) + + ", actual = " + Arrays.toString(actual)); + fail(); + } + } + + public static void assertFloatEq(float expected, float actual) { + if (Math.abs(actual - expected) > 1e-6) { + Log.v(TAG, "expected: " + expected + ", actual: " + actual); + fail(); + } + } +} diff --git a/tests/src/com/android/gallery3d/ui/GLMock.java b/tests/src/com/android/gallery3d/ui/GLMock.java new file mode 100644 index 000000000..c1fe53c62 --- /dev/null +++ b/tests/src/com/android/gallery3d/ui/GLMock.java @@ -0,0 +1,195 @@ +/* + * 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.ui; + +import java.nio.Buffer; +import java.util.HashMap; +import javax.microedition.khronos.opengles.GL10; +import javax.microedition.khronos.opengles.GL11; + +public class GLMock extends GLStub { + @SuppressWarnings("unused") + private static final String TAG = "GLMock"; + + // glClear + int mGLClearCalled; + int mGLClearMask; + // glBlendFunc + int mGLBlendFuncCalled; + int mGLBlendFuncSFactor; + int mGLBlendFuncDFactor; + // glColor4[fx] + int mGLColorCalled; + int mGLColor; + // glEnable, glDisable + boolean mGLBlendEnabled; + boolean mGLStencilEnabled; + // glEnableClientState + boolean mGLVertexArrayEnabled; + // glVertexPointer + PointerInfo mGLVertexPointer; + // glMatrixMode + int mGLMatrixMode = GL10.GL_MODELVIEW; + // glLoadMatrixf + float[] mGLModelViewMatrix = new float[16]; + float[] mGLProjectionMatrix = new float[16]; + // glBindTexture + int mGLBindTextureId; + // glTexEnvf + HashMap<Integer, Float> mGLTexEnv0 = new HashMap<Integer, Float>(); + HashMap<Integer, Float> mGLTexEnv1 = new HashMap<Integer, Float>(); + // glActiveTexture + int mGLActiveTexture = GL11.GL_TEXTURE0; + + @Override + public void glClear(int mask) { + mGLClearCalled++; + mGLClearMask = mask; + } + + @Override + public void glBlendFunc(int sfactor, int dfactor) { + mGLBlendFuncSFactor = sfactor; + mGLBlendFuncDFactor = dfactor; + mGLBlendFuncCalled++; + } + + @Override + public void glColor4f(float red, float green, float blue, + float alpha) { + mGLColorCalled++; + mGLColor = makeColor4f(red, green, blue, alpha); + } + + @Override + public void glColor4x(int red, int green, int blue, int alpha) { + mGLColorCalled++; + mGLColor = makeColor4x(red, green, blue, alpha); + } + + @Override + public void glEnable(int cap) { + if (cap == GL11.GL_BLEND) { + mGLBlendEnabled = true; + } else if (cap == GL11.GL_STENCIL_TEST) { + mGLStencilEnabled = true; + } + } + + @Override + public void glDisable(int cap) { + if (cap == GL11.GL_BLEND) { + mGLBlendEnabled = false; + } else if (cap == GL11.GL_STENCIL_TEST) { + mGLStencilEnabled = false; + } + } + + @Override + public void glEnableClientState(int array) { + if (array == GL10.GL_VERTEX_ARRAY) { + mGLVertexArrayEnabled = true; + } + } + + @Override + public void glVertexPointer(int size, int type, int stride, Buffer pointer) { + mGLVertexPointer = new PointerInfo(size, type, stride, pointer); + } + + @Override + public void glMatrixMode(int mode) { + mGLMatrixMode = mode; + } + + @Override + public void glLoadMatrixf(float[] m, int offset) { + if (mGLMatrixMode == GL10.GL_MODELVIEW) { + System.arraycopy(m, offset, mGLModelViewMatrix, 0, 16); + } else if (mGLMatrixMode == GL10.GL_PROJECTION) { + System.arraycopy(m, offset, mGLProjectionMatrix, 0, 16); + } + } + + @Override + public void glOrthof( + float left, float right, float bottom, float top, + float zNear, float zFar) { + float tx = -(right + left) / (right - left); + float ty = -(top + bottom) / (top - bottom); + float tz = - (zFar + zNear) / (zFar - zNear); + float[] m = new float[] { + 2 / (right - left), 0, 0, 0, + 0, 2 / (top - bottom), 0, 0, + 0, 0, -2 / (zFar - zNear), 0, + tx, ty, tz, 1 + }; + glLoadMatrixf(m, 0); + } + + @Override + public void glBindTexture(int target, int texture) { + if (target == GL11.GL_TEXTURE_2D) { + mGLBindTextureId = texture; + } + } + + @Override + public void glTexEnvf(int target, int pname, float param) { + if (target == GL11.GL_TEXTURE_ENV) { + if (mGLActiveTexture == GL11.GL_TEXTURE0) { + mGLTexEnv0.put(pname, param); + } else if (mGLActiveTexture == GL11.GL_TEXTURE1) { + mGLTexEnv1.put(pname, param); + } else { + throw new AssertionError(); + } + } + } + + public int getTexEnvi(int pname) { + return getTexEnvi(mGLActiveTexture, pname); + } + + public int getTexEnvi(int activeTexture, int pname) { + if (activeTexture == GL11.GL_TEXTURE0) { + return (int) mGLTexEnv0.get(pname).floatValue(); + } else if (activeTexture == GL11.GL_TEXTURE1) { + return (int) mGLTexEnv1.get(pname).floatValue(); + } else { + throw new AssertionError(); + } + } + + @Override + public void glActiveTexture(int texture) { + mGLActiveTexture = texture; + } + + public static int makeColor4f(float red, float green, float blue, + float alpha) { + return (Math.round(alpha * 255) << 24) | + (Math.round(red * 255) << 16) | + (Math.round(green * 255) << 8) | + Math.round(blue * 255); + } + + public static int makeColor4x(int red, int green, int blue, int alpha) { + final float X = 65536f; + return makeColor4f(red / X, green / X, blue / X, alpha / X); + } +} diff --git a/tests/src/com/android/gallery3d/ui/GLRootMock.java b/tests/src/com/android/gallery3d/ui/GLRootMock.java new file mode 100644 index 000000000..c83e94342 --- /dev/null +++ b/tests/src/com/android/gallery3d/ui/GLRootMock.java @@ -0,0 +1,37 @@ +/* + * 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.ui; + +import com.android.gallery3d.anim.CanvasAnimation; + +public class GLRootMock implements GLRoot { + int mRequestRenderCalled; + int mRequestLayoutContentPaneCalled; + + public void addOnGLIdleListener(OnGLIdleListener listener) {} + public void registerLaunchedAnimation(CanvasAnimation animation) {} + public void requestRender() { + mRequestRenderCalled++; + } + public void requestLayoutContentPane() { + mRequestLayoutContentPaneCalled++; + } + public boolean hasStencil() { return true; } + public void lockRenderThread() {} + public void unlockRenderThread() {} + public void setContentPane(GLView content) {} +} diff --git a/tests/src/com/android/gallery3d/ui/GLRootStub.java b/tests/src/com/android/gallery3d/ui/GLRootStub.java new file mode 100644 index 000000000..d6bc678d4 --- /dev/null +++ b/tests/src/com/android/gallery3d/ui/GLRootStub.java @@ -0,0 +1,30 @@ +/* + * 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.ui; + +import com.android.gallery3d.anim.CanvasAnimation; + +public class GLRootStub implements GLRoot { + public void addOnGLIdleListener(OnGLIdleListener listener) {} + public void registerLaunchedAnimation(CanvasAnimation animation) {} + public void requestRender() {} + public void requestLayoutContentPane() {} + public boolean hasStencil() { return true; } + public void lockRenderThread() {} + public void unlockRenderThread() {} + public void setContentPane(GLView content) {} +} diff --git a/tests/src/com/android/gallery3d/ui/GLStub.java b/tests/src/com/android/gallery3d/ui/GLStub.java new file mode 100644 index 000000000..2af73f905 --- /dev/null +++ b/tests/src/com/android/gallery3d/ui/GLStub.java @@ -0,0 +1,1490 @@ +/* + * 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.ui; + +import javax.microedition.khronos.opengles.GL; +import javax.microedition.khronos.opengles.GL10; +import javax.microedition.khronos.opengles.GL10Ext; +import javax.microedition.khronos.opengles.GL11; +import javax.microedition.khronos.opengles.GL11Ext; + +public class GLStub implements GL, GL10, GL10Ext, GL11, GL11Ext { + @SuppressWarnings("unused") + private static final String TAG = "GLStub"; + + public void glActiveTexture( + int texture + ){} + + public void glAlphaFunc( + int func, + float ref + ){} + + public void glAlphaFuncx( + int func, + int ref + ){} + + public void glBindTexture( + int target, + int texture + ){} + + public void glBlendFunc( + int sfactor, + int dfactor + ){} + + public void glClear( + int mask + ){} + + public void glClearColor( + float red, + float green, + float blue, + float alpha + ){} + + public void glClearColorx( + int red, + int green, + int blue, + int alpha + ){} + + public void glClearDepthf( + float depth + ){} + + public void glClearDepthx( + int depth + ){} + + public void glClearStencil( + int s + ){} + + public void glClientActiveTexture( + int texture + ){} + + public void glColor4f( + float red, + float green, + float blue, + float alpha + ){} + + public void glColor4x( + int red, + int green, + int blue, + int alpha + ){} + + public void glColorMask( + boolean red, + boolean green, + boolean blue, + boolean alpha + ){} + + public void glColorPointer( + int size, + int type, + int stride, + java.nio.Buffer pointer + ){} + + public void glCompressedTexImage2D( + int target, + int level, + int internalformat, + int width, + int height, + int border, + int imageSize, + java.nio.Buffer data + ){} + + public void glCompressedTexSubImage2D( + int target, + int level, + int xoffset, + int yoffset, + int width, + int height, + int format, + int imageSize, + java.nio.Buffer data + ){} + + public void glCopyTexImage2D( + int target, + int level, + int internalformat, + int x, + int y, + int width, + int height, + int border + ){} + + public void glCopyTexSubImage2D( + int target, + int level, + int xoffset, + int yoffset, + int x, + int y, + int width, + int height + ){} + + public void glCullFace( + int mode + ){} + + public void glDeleteTextures( + int n, + int[] textures, + int offset + ){} + + public void glDeleteTextures( + int n, + java.nio.IntBuffer textures + ){} + + public void glDepthFunc( + int func + ){} + + public void glDepthMask( + boolean flag + ){} + + public void glDepthRangef( + float zNear, + float zFar + ){} + + public void glDepthRangex( + int zNear, + int zFar + ){} + + public void glDisable( + int cap + ){} + + public void glDisableClientState( + int array + ){} + + public void glDrawArrays( + int mode, + int first, + int count + ){} + + public void glDrawElements( + int mode, + int count, + int type, + java.nio.Buffer indices + ){} + + public void glEnable( + int cap + ){} + + public void glEnableClientState( + int array + ){} + + public void glFinish( + ){} + + public void glFlush( + ){} + + public void glFogf( + int pname, + float param + ){} + + public void glFogfv( + int pname, + float[] params, + int offset + ){} + + public void glFogfv( + int pname, + java.nio.FloatBuffer params + ){} + + public void glFogx( + int pname, + int param + ){} + + public void glFogxv( + int pname, + int[] params, + int offset + ){} + + public void glFogxv( + int pname, + java.nio.IntBuffer params + ){} + + public void glFrontFace( + int mode + ){} + + public void glFrustumf( + float left, + float right, + float bottom, + float top, + float zNear, + float zFar + ){} + + public void glFrustumx( + int left, + int right, + int bottom, + int top, + int zNear, + int zFar + ){} + + public void glGenTextures( + int n, + int[] textures, + int offset + ){} + + public void glGenTextures( + int n, + java.nio.IntBuffer textures + ){} + + public int glGetError( + ){ throw new UnsupportedOperationException(); } + + public void glGetIntegerv( + int pname, + int[] params, + int offset + ){} + + public void glGetIntegerv( + int pname, + java.nio.IntBuffer params + ){} + + public String glGetString( + int name + ){ throw new UnsupportedOperationException(); } + + public void glHint( + int target, + int mode + ){} + + public void glLightModelf( + int pname, + float param + ){} + + public void glLightModelfv( + int pname, + float[] params, + int offset + ){} + + public void glLightModelfv( + int pname, + java.nio.FloatBuffer params + ){} + + public void glLightModelx( + int pname, + int param + ){} + + public void glLightModelxv( + int pname, + int[] params, + int offset + ){} + + public void glLightModelxv( + int pname, + java.nio.IntBuffer params + ){} + + public void glLightf( + int light, + int pname, + float param + ){} + + public void glLightfv( + int light, + int pname, + float[] params, + int offset + ){} + + public void glLightfv( + int light, + int pname, + java.nio.FloatBuffer params + ){} + + public void glLightx( + int light, + int pname, + int param + ){} + + public void glLightxv( + int light, + int pname, + int[] params, + int offset + ){} + + public void glLightxv( + int light, + int pname, + java.nio.IntBuffer params + ){} + + public void glLineWidth( + float width + ){} + + public void glLineWidthx( + int width + ){} + + public void glLoadIdentity( + ){} + + public void glLoadMatrixf( + float[] m, + int offset + ){} + + public void glLoadMatrixf( + java.nio.FloatBuffer m + ){} + + public void glLoadMatrixx( + int[] m, + int offset + ){} + + public void glLoadMatrixx( + java.nio.IntBuffer m + ){} + + public void glLogicOp( + int opcode + ){} + + public void glMaterialf( + int face, + int pname, + float param + ){} + + public void glMaterialfv( + int face, + int pname, + float[] params, + int offset + ){} + + public void glMaterialfv( + int face, + int pname, + java.nio.FloatBuffer params + ){} + + public void glMaterialx( + int face, + int pname, + int param + ){} + + public void glMaterialxv( + int face, + int pname, + int[] params, + int offset + ){} + + public void glMaterialxv( + int face, + int pname, + java.nio.IntBuffer params + ){} + + public void glMatrixMode( + int mode + ){} + + public void glMultMatrixf( + float[] m, + int offset + ){} + + public void glMultMatrixf( + java.nio.FloatBuffer m + ){} + + public void glMultMatrixx( + int[] m, + int offset + ){} + + public void glMultMatrixx( + java.nio.IntBuffer m + ){} + + public void glMultiTexCoord4f( + int target, + float s, + float t, + float r, + float q + ){} + + public void glMultiTexCoord4x( + int target, + int s, + int t, + int r, + int q + ){} + + public void glNormal3f( + float nx, + float ny, + float nz + ){} + + public void glNormal3x( + int nx, + int ny, + int nz + ){} + + public void glNormalPointer( + int type, + int stride, + java.nio.Buffer pointer + ){} + + public void glOrthof( + float left, + float right, + float bottom, + float top, + float zNear, + float zFar + ){} + + public void glOrthox( + int left, + int right, + int bottom, + int top, + int zNear, + int zFar + ){} + + public void glPixelStorei( + int pname, + int param + ){} + + public void glPointSize( + float size + ){} + + public void glPointSizex( + int size + ){} + + public void glPolygonOffset( + float factor, + float units + ){} + + public void glPolygonOffsetx( + int factor, + int units + ){} + + public void glPopMatrix( + ){} + + public void glPushMatrix( + ){} + + public void glReadPixels( + int x, + int y, + int width, + int height, + int format, + int type, + java.nio.Buffer pixels + ){} + + public void glRotatef( + float angle, + float x, + float y, + float z + ){} + + public void glRotatex( + int angle, + int x, + int y, + int z + ){} + + public void glSampleCoverage( + float value, + boolean invert + ){} + + public void glSampleCoveragex( + int value, + boolean invert + ){} + + public void glScalef( + float x, + float y, + float z + ){} + + public void glScalex( + int x, + int y, + int z + ){} + + public void glScissor( + int x, + int y, + int width, + int height + ){} + + public void glShadeModel( + int mode + ){} + + public void glStencilFunc( + int func, + int ref, + int mask + ){} + + public void glStencilMask( + int mask + ){} + + public void glStencilOp( + int fail, + int zfail, + int zpass + ){} + + public void glTexCoordPointer( + int size, + int type, + int stride, + java.nio.Buffer pointer + ){} + + public void glTexEnvf( + int target, + int pname, + float param + ){} + + public void glTexEnvfv( + int target, + int pname, + float[] params, + int offset + ){} + + public void glTexEnvfv( + int target, + int pname, + java.nio.FloatBuffer params + ){} + + public void glTexEnvx( + int target, + int pname, + int param + ){} + + public void glTexEnvxv( + int target, + int pname, + int[] params, + int offset + ){} + + public void glTexEnvxv( + int target, + int pname, + java.nio.IntBuffer params + ){} + + public void glTexImage2D( + int target, + int level, + int internalformat, + int width, + int height, + int border, + int format, + int type, + java.nio.Buffer pixels + ){} + + public void glTexParameterf( + int target, + int pname, + float param + ){} + + public void glTexParameterx( + int target, + int pname, + int param + ){} + + public void glTexSubImage2D( + int target, + int level, + int xoffset, + int yoffset, + int width, + int height, + int format, + int type, + java.nio.Buffer pixels + ){} + + public void glTranslatef( + float x, + float y, + float z + ){} + + public void glTranslatex( + int x, + int y, + int z + ){} + + public void glVertexPointer( + int size, + int type, + int stride, + java.nio.Buffer pointer + ){} + + public void glViewport( + int x, + int y, + int width, + int height + ){} + + public int glQueryMatrixxOES( + int[] mantissa, + int mantissaOffset, + int[] exponent, + int exponentOffset + ){ throw new UnsupportedOperationException(); } + + public int glQueryMatrixxOES( + java.nio.IntBuffer mantissa, + java.nio.IntBuffer exponent + ){ throw new UnsupportedOperationException(); } + + public void glGetPointerv(int pname, java.nio.Buffer[] params){} + public void glBindBuffer( + int target, + int buffer + ){} + + public void glBufferData( + int target, + int size, + java.nio.Buffer data, + int usage + ){} + + public void glBufferSubData( + int target, + int offset, + int size, + java.nio.Buffer data + ){} + + public void glClipPlanef( + int plane, + float[] equation, + int offset + ){} + + public void glClipPlanef( + int plane, + java.nio.FloatBuffer equation + ){} + + public void glClipPlanex( + int plane, + int[] equation, + int offset + ){} + + public void glClipPlanex( + int plane, + java.nio.IntBuffer equation + ){} + + public void glColor4ub( + byte red, + byte green, + byte blue, + byte alpha + ){} + + public void glColorPointer( + int size, + int type, + int stride, + int offset + ){} + + public void glDeleteBuffers( + int n, + int[] buffers, + int offset + ){} + + public void glDeleteBuffers( + int n, + java.nio.IntBuffer buffers + ){} + + public void glDrawElements( + int mode, + int count, + int type, + int offset + ){} + + public void glGenBuffers( + int n, + int[] buffers, + int offset + ){} + + public void glGenBuffers( + int n, + java.nio.IntBuffer buffers + ){} + + public void glGetBooleanv( + int pname, + boolean[] params, + int offset + ){} + + public void glGetBooleanv( + int pname, + java.nio.IntBuffer params + ){} + + public void glGetBufferParameteriv( + int target, + int pname, + int[] params, + int offset + ){} + + public void glGetBufferParameteriv( + int target, + int pname, + java.nio.IntBuffer params + ){} + + public void glGetClipPlanef( + int pname, + float[] eqn, + int offset + ){} + + public void glGetClipPlanef( + int pname, + java.nio.FloatBuffer eqn + ){} + + public void glGetClipPlanex( + int pname, + int[] eqn, + int offset + ){} + + public void glGetClipPlanex( + int pname, + java.nio.IntBuffer eqn + ){} + + public void glGetFixedv( + int pname, + int[] params, + int offset + ){} + + public void glGetFixedv( + int pname, + java.nio.IntBuffer params + ){} + + public void glGetFloatv( + int pname, + float[] params, + int offset + ){} + + public void glGetFloatv( + int pname, + java.nio.FloatBuffer params + ){} + + public void glGetLightfv( + int light, + int pname, + float[] params, + int offset + ){} + + public void glGetLightfv( + int light, + int pname, + java.nio.FloatBuffer params + ){} + + public void glGetLightxv( + int light, + int pname, + int[] params, + int offset + ){} + + public void glGetLightxv( + int light, + int pname, + java.nio.IntBuffer params + ){} + + public void glGetMaterialfv( + int face, + int pname, + float[] params, + int offset + ){} + + public void glGetMaterialfv( + int face, + int pname, + java.nio.FloatBuffer params + ){} + + public void glGetMaterialxv( + int face, + int pname, + int[] params, + int offset + ){} + + public void glGetMaterialxv( + int face, + int pname, + java.nio.IntBuffer params + ){} + + public void glGetTexEnviv( + int env, + int pname, + int[] params, + int offset + ){} + + public void glGetTexEnviv( + int env, + int pname, + java.nio.IntBuffer params + ){} + + public void glGetTexEnvxv( + int env, + int pname, + int[] params, + int offset + ){} + + public void glGetTexEnvxv( + int env, + int pname, + java.nio.IntBuffer params + ){} + + public void glGetTexParameterfv( + int target, + int pname, + float[] params, + int offset + ){} + + public void glGetTexParameterfv( + int target, + int pname, + java.nio.FloatBuffer params + ){} + + public void glGetTexParameteriv( + int target, + int pname, + int[] params, + int offset + ){} + + public void glGetTexParameteriv( + int target, + int pname, + java.nio.IntBuffer params + ){} + + public void glGetTexParameterxv( + int target, + int pname, + int[] params, + int offset + ){} + + public void glGetTexParameterxv( + int target, + int pname, + java.nio.IntBuffer params + ){} + + public boolean glIsBuffer( + int buffer + ){ throw new UnsupportedOperationException(); } + + public boolean glIsEnabled( + int cap + ){ throw new UnsupportedOperationException(); } + + public boolean glIsTexture( + int texture + ){ throw new UnsupportedOperationException(); } + + public void glNormalPointer( + int type, + int stride, + int offset + ){} + + public void glPointParameterf( + int pname, + float param + ){} + + public void glPointParameterfv( + int pname, + float[] params, + int offset + ){} + + public void glPointParameterfv( + int pname, + java.nio.FloatBuffer params + ){} + + public void glPointParameterx( + int pname, + int param + ){} + + public void glPointParameterxv( + int pname, + int[] params, + int offset + ){} + + public void glPointParameterxv( + int pname, + java.nio.IntBuffer params + ){} + + public void glPointSizePointerOES( + int type, + int stride, + java.nio.Buffer pointer + ){} + + public void glTexCoordPointer( + int size, + int type, + int stride, + int offset + ){} + + public void glTexEnvi( + int target, + int pname, + int param + ){} + + public void glTexEnviv( + int target, + int pname, + int[] params, + int offset + ){} + + public void glTexEnviv( + int target, + int pname, + java.nio.IntBuffer params + ){} + + public void glTexParameterfv( + int target, + int pname, + float[] params, + int offset + ){} + + public void glTexParameterfv( + int target, + int pname, + java.nio.FloatBuffer params + ){} + + public void glTexParameteri( + int target, + int pname, + int param + ){} + + public void glTexParameteriv( + int target, + int pname, + int[] params, + int offset + ){} + + public void glTexParameteriv( + int target, + int pname, + java.nio.IntBuffer params + ){} + + public void glTexParameterxv( + int target, + int pname, + int[] params, + int offset + ){} + + public void glTexParameterxv( + int target, + int pname, + java.nio.IntBuffer params + ){} + + public void glVertexPointer( + int size, + int type, + int stride, + int offset + ){} + + public void glCurrentPaletteMatrixOES( + int matrixpaletteindex + ){} + + public void glDrawTexfOES( + float x, + float y, + float z, + float width, + float height + ){} + + public void glDrawTexfvOES( + float[] coords, + int offset + ){} + + public void glDrawTexfvOES( + java.nio.FloatBuffer coords + ){} + + public void glDrawTexiOES( + int x, + int y, + int z, + int width, + int height + ){} + + public void glDrawTexivOES( + int[] coords, + int offset + ){} + + public void glDrawTexivOES( + java.nio.IntBuffer coords + ){} + + public void glDrawTexsOES( + short x, + short y, + short z, + short width, + short height + ){} + + public void glDrawTexsvOES( + short[] coords, + int offset + ){} + + public void glDrawTexsvOES( + java.nio.ShortBuffer coords + ){} + + public void glDrawTexxOES( + int x, + int y, + int z, + int width, + int height + ){} + + public void glDrawTexxvOES( + int[] coords, + int offset + ){} + + public void glDrawTexxvOES( + java.nio.IntBuffer coords + ){} + + public void glLoadPaletteFromModelViewMatrixOES( + ){} + + public void glMatrixIndexPointerOES( + int size, + int type, + int stride, + java.nio.Buffer pointer + ){} + + public void glMatrixIndexPointerOES( + int size, + int type, + int stride, + int offset + ){} + + public void glWeightPointerOES( + int size, + int type, + int stride, + java.nio.Buffer pointer + ){} + + public void glWeightPointerOES( + int size, + int type, + int stride, + int offset + ){} + + public void glBindFramebufferOES( + int target, + int framebuffer + ){} + + public void glBindRenderbufferOES( + int target, + int renderbuffer + ){} + + public void glBlendEquation( + int mode + ){} + + public void glBlendEquationSeparate( + int modeRGB, + int modeAlpha + ){} + + public void glBlendFuncSeparate( + int srcRGB, + int dstRGB, + int srcAlpha, + int dstAlpha + ){} + + public int glCheckFramebufferStatusOES( + int target + ){ throw new UnsupportedOperationException(); } + + public void glDeleteFramebuffersOES( + int n, + int[] framebuffers, + int offset + ){} + + public void glDeleteFramebuffersOES( + int n, + java.nio.IntBuffer framebuffers + ){} + + public void glDeleteRenderbuffersOES( + int n, + int[] renderbuffers, + int offset + ){} + + public void glDeleteRenderbuffersOES( + int n, + java.nio.IntBuffer renderbuffers + ){} + + public void glFramebufferRenderbufferOES( + int target, + int attachment, + int renderbuffertarget, + int renderbuffer + ){} + + public void glFramebufferTexture2DOES( + int target, + int attachment, + int textarget, + int texture, + int level + ){} + + public void glGenerateMipmapOES( + int target + ){} + + public void glGenFramebuffersOES( + int n, + int[] framebuffers, + int offset + ){} + + public void glGenFramebuffersOES( + int n, + java.nio.IntBuffer framebuffers + ){} + + public void glGenRenderbuffersOES( + int n, + int[] renderbuffers, + int offset + ){} + + public void glGenRenderbuffersOES( + int n, + java.nio.IntBuffer renderbuffers + ){} + + public void glGetFramebufferAttachmentParameterivOES( + int target, + int attachment, + int pname, + int[] params, + int offset + ){} + + public void glGetFramebufferAttachmentParameterivOES( + int target, + int attachment, + int pname, + java.nio.IntBuffer params + ){} + + public void glGetRenderbufferParameterivOES( + int target, + int pname, + int[] params, + int offset + ){} + + public void glGetRenderbufferParameterivOES( + int target, + int pname, + java.nio.IntBuffer params + ){} + + public void glGetTexGenfv( + int coord, + int pname, + float[] params, + int offset + ){} + + public void glGetTexGenfv( + int coord, + int pname, + java.nio.FloatBuffer params + ){} + + public void glGetTexGeniv( + int coord, + int pname, + int[] params, + int offset + ){} + + public void glGetTexGeniv( + int coord, + int pname, + java.nio.IntBuffer params + ){} + + public void glGetTexGenxv( + int coord, + int pname, + int[] params, + int offset + ){} + + public void glGetTexGenxv( + int coord, + int pname, + java.nio.IntBuffer params + ){} + + public boolean glIsFramebufferOES( + int framebuffer + ){ throw new UnsupportedOperationException(); } + + public boolean glIsRenderbufferOES( + int renderbuffer + ){ throw new UnsupportedOperationException(); } + + public void glRenderbufferStorageOES( + int target, + int internalformat, + int width, + int height + ){} + + public void glTexGenf( + int coord, + int pname, + float param + ){} + + public void glTexGenfv( + int coord, + int pname, + float[] params, + int offset + ){} + + public void glTexGenfv( + int coord, + int pname, + java.nio.FloatBuffer params + ){} + + public void glTexGeni( + int coord, + int pname, + int param + ){} + + public void glTexGeniv( + int coord, + int pname, + int[] params, + int offset + ){} + + public void glTexGeniv( + int coord, + int pname, + java.nio.IntBuffer params + ){} + + public void glTexGenx( + int coord, + int pname, + int param + ){} + + public void glTexGenxv( + int coord, + int pname, + int[] params, + int offset + ){} + + public void glTexGenxv( + int coord, + int pname, + java.nio.IntBuffer params + ){} +} diff --git a/tests/src/com/android/gallery3d/ui/GLViewMock.java b/tests/src/com/android/gallery3d/ui/GLViewMock.java new file mode 100644 index 000000000..7b941daad --- /dev/null +++ b/tests/src/com/android/gallery3d/ui/GLViewMock.java @@ -0,0 +1,85 @@ +/* + * 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.ui; + +class GLViewMock extends GLView { + // onAttachToRoot + int mOnAttachCalled; + GLRoot mRoot; + // onDetachFromRoot + int mOnDetachCalled; + // onVisibilityChanged + int mOnVisibilityChangedCalled; + // onLayout + int mOnLayoutCalled; + boolean mOnLayoutChangeSize; + // renderBackground + int mRenderBackgroundCalled; + // onMeasure + int mOnMeasureCalled; + int mOnMeasureWidthSpec; + int mOnMeasureHeightSpec; + + @Override + public void onAttachToRoot(GLRoot root) { + mRoot = root; + mOnAttachCalled++; + super.onAttachToRoot(root); + } + + @Override + public void onDetachFromRoot() { + mRoot = null; + mOnDetachCalled++; + super.onDetachFromRoot(); + } + + @Override + protected void onVisibilityChanged(int visibility) { + mOnVisibilityChangedCalled++; + } + + @Override + protected void onLayout(boolean changeSize, int left, int top, + int right, int bottom) { + mOnLayoutCalled++; + mOnLayoutChangeSize = changeSize; + // call children's layout. + for (int i = 0, n = getComponentCount(); i < n; ++i) { + GLView item = getComponent(i); + item.layout(left, top, right, bottom); + } + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + mOnMeasureCalled++; + mOnMeasureWidthSpec = widthSpec; + mOnMeasureHeightSpec = heightSpec; + // call children's measure. + for (int i = 0, n = getComponentCount(); i < n; ++i) { + GLView item = getComponent(i); + item.measure(widthSpec, heightSpec); + } + setMeasuredSize(widthSpec, heightSpec); + } + + @Override + protected void renderBackground(GLCanvas view) { + mRenderBackgroundCalled++; + } +} diff --git a/tests/src/com/android/gallery3d/ui/GLViewTest.java b/tests/src/com/android/gallery3d/ui/GLViewTest.java new file mode 100644 index 000000000..a9377bfee --- /dev/null +++ b/tests/src/com/android/gallery3d/ui/GLViewTest.java @@ -0,0 +1,424 @@ +/* + * 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.ui; + +import android.graphics.Rect; +import android.test.suitebuilder.annotation.SmallTest; +import android.view.MotionEvent; + +import junit.framework.TestCase; + +@SmallTest +public class GLViewTest extends TestCase { + @SuppressWarnings("unused") + private static final String TAG = "GLViewTest"; + + @SmallTest + public void testVisibility() { + GLViewMock a = new GLViewMock(); + assertEquals(GLView.VISIBLE, a.getVisibility()); + assertEquals(0, a.mOnVisibilityChangedCalled); + a.setVisibility(GLView.INVISIBLE); + assertEquals(GLView.INVISIBLE, a.getVisibility()); + assertEquals(1, a.mOnVisibilityChangedCalled); + a.setVisibility(GLView.VISIBLE); + assertEquals(GLView.VISIBLE, a.getVisibility()); + assertEquals(2, a.mOnVisibilityChangedCalled); + } + + @SmallTest + public void testComponents() { + GLView view = new GLView(); + assertEquals(0, view.getComponentCount()); + try { + view.getComponent(0); + fail(); + } catch (IndexOutOfBoundsException ex) { + // expected + } + + GLView x = new GLView(); + GLView y = new GLView(); + view.addComponent(x); + view.addComponent(y); + assertEquals(2, view.getComponentCount()); + assertSame(x, view.getComponent(0)); + assertSame(y, view.getComponent(1)); + view.removeComponent(x); + assertSame(y, view.getComponent(0)); + try { + view.getComponent(1); + fail(); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + view.addComponent(y); + fail(); + } catch (IllegalStateException ex) { + // expected + } + view.addComponent(x); + view.removeAllComponents(); + assertEquals(0, view.getComponentCount()); + } + + @SmallTest + public void testBounds() { + GLView view = new GLView(); + + assertEquals(0, view.getWidth()); + assertEquals(0, view.getHeight()); + + Rect b = view.bounds(); + assertEquals(0, b.left); + assertEquals(0, b.top); + assertEquals(0, b.right); + assertEquals(0, b.bottom); + + view.layout(10, 20, 30, 100); + assertEquals(20, view.getWidth()); + assertEquals(80, view.getHeight()); + + b = view.bounds(); + assertEquals(10, b.left); + assertEquals(20, b.top); + assertEquals(30, b.right); + assertEquals(100, b.bottom); + } + + @SmallTest + public void testPaddings() { + GLView view = new GLView(); + + Rect p = view.getPaddings(); + assertEquals(0, p.left); + assertEquals(0, p.top); + assertEquals(0, p.right); + assertEquals(0, p.bottom); + + view.setPaddings(10, 20, 30, 100); + p = view.getPaddings(); + assertEquals(10, p.left); + assertEquals(20, p.top); + assertEquals(30, p.right); + assertEquals(100, p.bottom); + + p = new Rect(11, 22, 33, 104); + view.setPaddings(p); + p = view.getPaddings(); + assertEquals(11, p.left); + assertEquals(22, p.top); + assertEquals(33, p.right); + assertEquals(104, p.bottom); + } + + @SmallTest + public void testParent() { + GLView a = new GLView(); + GLView b = new GLView(); + assertNull(b.mParent); + a.addComponent(b); + assertSame(a, b.mParent); + a.removeComponent(b); + assertNull(b.mParent); + } + + @SmallTest + public void testRoot() { + GLViewMock a = new GLViewMock(); + GLViewMock b = new GLViewMock(); + GLRoot r = new GLRootStub(); + GLRoot r2 = new GLRootStub(); + a.addComponent(b); + + // Attach to root r + assertEquals(0, a.mOnAttachCalled); + assertEquals(0, b.mOnAttachCalled); + a.attachToRoot(r); + assertEquals(1, a.mOnAttachCalled); + assertEquals(1, b.mOnAttachCalled); + assertSame(r, a.getGLRoot()); + assertSame(r, b.getGLRoot()); + + // Detach from r + assertEquals(0, a.mOnDetachCalled); + assertEquals(0, b.mOnDetachCalled); + a.detachFromRoot(); + assertEquals(1, a.mOnDetachCalled); + assertEquals(1, b.mOnDetachCalled); + + // Attach to another root r2 + assertEquals(1, a.mOnAttachCalled); + assertEquals(1, b.mOnAttachCalled); + a.attachToRoot(r2); + assertEquals(2, a.mOnAttachCalled); + assertEquals(2, b.mOnAttachCalled); + assertSame(r2, a.getGLRoot()); + assertSame(r2, b.getGLRoot()); + + // Detach from r2 + assertEquals(1, a.mOnDetachCalled); + assertEquals(1, b.mOnDetachCalled); + a.detachFromRoot(); + assertEquals(2, a.mOnDetachCalled); + assertEquals(2, b.mOnDetachCalled); + } + + @SmallTest + public void testRoot2() { + GLView a = new GLViewMock(); + GLViewMock b = new GLViewMock(); + GLRoot r = new GLRootStub(); + + a.attachToRoot(r); + + assertEquals(0, b.mOnAttachCalled); + a.addComponent(b); + assertEquals(1, b.mOnAttachCalled); + + assertEquals(0, b.mOnDetachCalled); + a.removeComponent(b); + assertEquals(1, b.mOnDetachCalled); + } + + @SmallTest + public void testInvalidate() { + GLView a = new GLView(); + GLRootMock r = new GLRootMock(); + a.attachToRoot(r); + assertEquals(0, r.mRequestRenderCalled); + a.invalidate(); + assertEquals(1, r.mRequestRenderCalled); + } + + @SmallTest + public void testRequestLayout() { + GLView a = new GLView(); + GLView b = new GLView(); + GLRootMock r = new GLRootMock(); + a.attachToRoot(r); + a.addComponent(b); + assertEquals(0, r.mRequestLayoutContentPaneCalled); + b.requestLayout(); + assertEquals(1, r.mRequestLayoutContentPaneCalled); + } + + @SmallTest + public void testLayout() { + GLViewMock a = new GLViewMock(); + GLViewMock b = new GLViewMock(); + GLViewMock c = new GLViewMock(); + GLRootMock r = new GLRootMock(); + + a.attachToRoot(r); + a.addComponent(b); + a.addComponent(c); + + assertEquals(0, a.mOnLayoutCalled); + a.layout(10, 20, 60, 100); + assertEquals(1, a.mOnLayoutCalled); + assertEquals(1, b.mOnLayoutCalled); + assertEquals(1, c.mOnLayoutCalled); + assertTrue(a.mOnLayoutChangeSize); + assertTrue(b.mOnLayoutChangeSize); + assertTrue(c.mOnLayoutChangeSize); + + // same size should not trigger onLayout + a.layout(10, 20, 60, 100); + assertEquals(1, a.mOnLayoutCalled); + + // unless someone requested it, but only those on the path + // to the requester. + assertEquals(0, r.mRequestLayoutContentPaneCalled); + b.requestLayout(); + a.layout(10, 20, 60, 100); + assertEquals(1, r.mRequestLayoutContentPaneCalled); + assertEquals(2, a.mOnLayoutCalled); + assertEquals(2, b.mOnLayoutCalled); + assertEquals(1, c.mOnLayoutCalled); + } + + @SmallTest + public void testRender() { + GLViewMock a = new GLViewMock(); + GLViewMock b = new GLViewMock(); + + a.addComponent(b); + GLCanvasStub canvas = new GLCanvasStub(); + assertEquals(0, a.mRenderBackgroundCalled); + assertEquals(0, b.mRenderBackgroundCalled); + a.render(canvas); + assertEquals(1, a.mRenderBackgroundCalled); + assertEquals(1, b.mRenderBackgroundCalled); + } + + @SmallTest + public void testMeasure() { + GLViewMock a = new GLViewMock(); + GLViewMock b = new GLViewMock(); + GLViewMock c = new GLViewMock(); + GLRootMock r = new GLRootMock(); + + a.addComponent(b); + a.addComponent(c); + a.attachToRoot(r); + + assertEquals(0, a.mOnMeasureCalled); + a.measure(100, 200); + assertEquals(1, a.mOnMeasureCalled); + assertEquals(1, b.mOnMeasureCalled); + assertEquals(100, a.mOnMeasureWidthSpec); + assertEquals(200, a.mOnMeasureHeightSpec); + assertEquals(100, b.mOnMeasureWidthSpec); + assertEquals(200, b.mOnMeasureHeightSpec); + assertEquals(100, a.getMeasuredWidth()); + assertEquals(200, b.getMeasuredHeight()); + + // same spec should not trigger onMeasure + a.measure(100, 200); + assertEquals(1, a.mOnMeasureCalled); + + // unless someone requested it, but only those on the path + // to the requester. + b.requestLayout(); + a.measure(100, 200); + assertEquals(2, a.mOnMeasureCalled); + assertEquals(2, b.mOnMeasureCalled); + assertEquals(1, c.mOnMeasureCalled); + } + + class MyGLView extends GLView { + private int mWidth; + int mOnTouchCalled; + int mOnTouchX; + int mOnTouchY; + int mOnTouchAction; + + public MyGLView(int width) { + mWidth = width; + } + + @Override + protected void onLayout(boolean changeSize, int left, int top, + int right, int bottom) { + // layout children from left to right + // call children's layout. + int x = 0; + for (int i = 0, n = getComponentCount(); i < n; ++i) { + GLView item = getComponent(i); + item.measure(0, 0); + int w = item.getMeasuredWidth(); + int h = item.getMeasuredHeight(); + item.layout(x, 0, x + w, h); + x += w; + } + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + setMeasuredSize(mWidth, 100); + } + + @Override + protected boolean onTouch(MotionEvent event) { + mOnTouchCalled++; + mOnTouchX = (int) event.getX(); + mOnTouchY = (int) event.getY(); + mOnTouchAction = event.getAction(); + return true; + } + } + + private MotionEvent NewMotionEvent(int action, int x, int y) { + return MotionEvent.obtain(0, 0, action, x, y, 0); + } + + @SmallTest + public void testTouchEvent() { + // We construct a tree with four nodes. Only the x coordinate is used: + // A = [0..............................300) + // B = [0......100) + // C = [100......200) + // D = [100..150) + + MyGLView a = new MyGLView(300); + MyGLView b = new MyGLView(100); + MyGLView c = new MyGLView(100); + MyGLView d = new MyGLView(50); + GLRoot r = new GLRootStub(); + + a.addComponent(b); + a.addComponent(c); + c.addComponent(d); + a.attachToRoot(r); + a.layout(0, 0, 300, 100); + + int DOWN = MotionEvent.ACTION_DOWN; + int UP = MotionEvent.ACTION_UP; + int MOVE = MotionEvent.ACTION_MOVE; + int CANCEL = MotionEvent.ACTION_CANCEL; + + // simple case + assertEquals(0, a.mOnTouchCalled); + a.dispatchTouchEvent(NewMotionEvent(DOWN, 250, 0)); + assertEquals(DOWN, a.mOnTouchAction); + a.dispatchTouchEvent(NewMotionEvent(UP, 250, 0)); + assertEquals(UP, a.mOnTouchAction); + assertEquals(2, a.mOnTouchCalled); + + // pass to a child, check the location is offseted. + assertEquals(0, c.mOnTouchCalled); + a.dispatchTouchEvent(NewMotionEvent(DOWN, 175, 0)); + a.dispatchTouchEvent(NewMotionEvent(UP, 175, 0)); + assertEquals(75, c.mOnTouchX); + assertEquals(0, c.mOnTouchY); + assertEquals(2, c.mOnTouchCalled); + assertEquals(2, a.mOnTouchCalled); + + // motion target cancel event + assertEquals(0, d.mOnTouchCalled); + a.dispatchTouchEvent(NewMotionEvent(DOWN, 125, 0)); + assertEquals(1, d.mOnTouchCalled); + a.dispatchTouchEvent(NewMotionEvent(MOVE, 250, 0)); + assertEquals(2, d.mOnTouchCalled); + a.dispatchTouchEvent(NewMotionEvent(MOVE, 50, 0)); + assertEquals(3, d.mOnTouchCalled); + a.dispatchTouchEvent(NewMotionEvent(DOWN, 175, 0)); + assertEquals(4, d.mOnTouchCalled); + assertEquals(CANCEL, d.mOnTouchAction); + assertEquals(3, c.mOnTouchCalled); + assertEquals(DOWN, c.mOnTouchAction); + a.dispatchTouchEvent(NewMotionEvent(UP, 175, 0)); + + // motion target is removed + assertEquals(4, d.mOnTouchCalled); + a.dispatchTouchEvent(NewMotionEvent(DOWN, 125, 0)); + assertEquals(5, d.mOnTouchCalled); + a.removeComponent(c); + assertEquals(6, d.mOnTouchCalled); + assertEquals(CANCEL, d.mOnTouchAction); + + // invisible component should not get events + assertEquals(2, a.mOnTouchCalled); + assertEquals(0, b.mOnTouchCalled); + b.setVisibility(GLView.INVISIBLE); + a.dispatchTouchEvent(NewMotionEvent(DOWN, 50, 0)); + assertEquals(3, a.mOnTouchCalled); + assertEquals(0, b.mOnTouchCalled); + } +} diff --git a/tests/src/com/android/gallery3d/ui/PointerInfo.java b/tests/src/com/android/gallery3d/ui/PointerInfo.java new file mode 100644 index 000000000..6c78556e1 --- /dev/null +++ b/tests/src/com/android/gallery3d/ui/PointerInfo.java @@ -0,0 +1,222 @@ +/* + * 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.ui; + +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.CharBuffer; +import java.nio.DoubleBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.LongBuffer; +import java.nio.ShortBuffer; + +import javax.microedition.khronos.opengles.GL10; + +public class PointerInfo { + + /** + * The number of coordinates per vertex. 1..4 + */ + public int mSize; + + /** + * The type of each coordinate. + */ + public int mType; + + /** + * The byte offset between consecutive vertices. 0 means mSize * + * sizeof(mType) + */ + public int mStride; + public Buffer mPointer; + public ByteBuffer mTempByteBuffer; + + public PointerInfo(int size, int type, int stride, Buffer pointer) { + mSize = size; + mType = type; + mStride = stride; + mPointer = pointer; + } + + private int getStride() { + return mStride > 0 ? mStride : sizeof(mType) * mSize; + } + + public void bindByteBuffer() { + mTempByteBuffer = mPointer == null ? null : toByteBuffer(-1, mPointer); + } + + public void unbindByteBuffer() { + mTempByteBuffer = null; + } + + private static int sizeof(int type) { + switch (type) { + case GL10.GL_UNSIGNED_BYTE: + return 1; + case GL10.GL_BYTE: + return 1; + case GL10.GL_SHORT: + return 2; + case GL10.GL_FIXED: + return 4; + case GL10.GL_FLOAT: + return 4; + default: + return 0; + } + } + + private static ByteBuffer toByteBuffer(int byteCount, Buffer input) { + ByteBuffer result = null; + boolean convertWholeBuffer = (byteCount < 0); + if (input instanceof ByteBuffer) { + ByteBuffer input2 = (ByteBuffer) input; + int position = input2.position(); + if (convertWholeBuffer) { + byteCount = input2.limit() - position; + } + result = ByteBuffer.allocate(byteCount).order(input2.order()); + for (int i = 0; i < byteCount; i++) { + result.put(input2.get()); + } + input2.position(position); + } else if (input instanceof CharBuffer) { + CharBuffer input2 = (CharBuffer) input; + int position = input2.position(); + if (convertWholeBuffer) { + byteCount = (input2.limit() - position) * 2; + } + result = ByteBuffer.allocate(byteCount).order(input2.order()); + CharBuffer result2 = result.asCharBuffer(); + for (int i = 0; i < byteCount / 2; i++) { + result2.put(input2.get()); + } + input2.position(position); + } else if (input instanceof ShortBuffer) { + ShortBuffer input2 = (ShortBuffer) input; + int position = input2.position(); + if (convertWholeBuffer) { + byteCount = (input2.limit() - position)* 2; + } + result = ByteBuffer.allocate(byteCount).order(input2.order()); + ShortBuffer result2 = result.asShortBuffer(); + for (int i = 0; i < byteCount / 2; i++) { + result2.put(input2.get()); + } + input2.position(position); + } else if (input instanceof IntBuffer) { + IntBuffer input2 = (IntBuffer) input; + int position = input2.position(); + if (convertWholeBuffer) { + byteCount = (input2.limit() - position) * 4; + } + result = ByteBuffer.allocate(byteCount).order(input2.order()); + IntBuffer result2 = result.asIntBuffer(); + for (int i = 0; i < byteCount / 4; i++) { + result2.put(input2.get()); + } + input2.position(position); + } else if (input instanceof FloatBuffer) { + FloatBuffer input2 = (FloatBuffer) input; + int position = input2.position(); + if (convertWholeBuffer) { + byteCount = (input2.limit() - position) * 4; + } + result = ByteBuffer.allocate(byteCount).order(input2.order()); + FloatBuffer result2 = result.asFloatBuffer(); + for (int i = 0; i < byteCount / 4; i++) { + result2.put(input2.get()); + } + input2.position(position); + } else if (input instanceof DoubleBuffer) { + DoubleBuffer input2 = (DoubleBuffer) input; + int position = input2.position(); + if (convertWholeBuffer) { + byteCount = (input2.limit() - position) * 8; + } + result = ByteBuffer.allocate(byteCount).order(input2.order()); + DoubleBuffer result2 = result.asDoubleBuffer(); + for (int i = 0; i < byteCount / 8; i++) { + result2.put(input2.get()); + } + input2.position(position); + } else if (input instanceof LongBuffer) { + LongBuffer input2 = (LongBuffer) input; + int position = input2.position(); + if (convertWholeBuffer) { + byteCount = (input2.limit() - position) * 8; + } + result = ByteBuffer.allocate(byteCount).order(input2.order()); + LongBuffer result2 = result.asLongBuffer(); + for (int i = 0; i < byteCount / 8; i++) { + result2.put(input2.get()); + } + input2.position(position); + } else { + throw new RuntimeException("Unimplemented Buffer subclass."); + } + result.rewind(); + // The OpenGL API will interpret the result in hardware byte order, + // so we better do that as well: + result.order(ByteOrder.nativeOrder()); + return result; + } + + public void getArrayElement(int index, double[] result) { + if (mTempByteBuffer == null) { + throw new IllegalArgumentException("undefined pointer"); + } + if (mStride < 0) { + throw new IllegalArgumentException("invalid stride"); + } + + int stride = getStride(); + ByteBuffer byteBuffer = mTempByteBuffer; + int size = mSize; + int type = mType; + int sizeofType = sizeof(type); + int byteOffset = stride * index; + + for (int i = 0; i < size; i++) { + switch (type) { + case GL10.GL_BYTE: + case GL10.GL_UNSIGNED_BYTE: + result[i] = byteBuffer.get(byteOffset); + break; + case GL10.GL_SHORT: + ShortBuffer shortBuffer = byteBuffer.asShortBuffer(); + result[i] = shortBuffer.get(byteOffset / 2); + break; + case GL10.GL_FIXED: + IntBuffer intBuffer = byteBuffer.asIntBuffer(); + result[i] = intBuffer.get(byteOffset / 4); + break; + case GL10.GL_FLOAT: + FloatBuffer floatBuffer = byteBuffer.asFloatBuffer(); + result[i] = floatBuffer.get(byteOffset / 4); + break; + default: + throw new UnsupportedOperationException("unknown type"); + } + byteOffset += sizeofType; + } + } +} diff --git a/tests/src/com/android/gallery3d/ui/TextureTest.java b/tests/src/com/android/gallery3d/ui/TextureTest.java new file mode 100644 index 000000000..fb26060bc --- /dev/null +++ b/tests/src/com/android/gallery3d/ui/TextureTest.java @@ -0,0 +1,208 @@ +/* + * 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.ui; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.test.suitebuilder.annotation.SmallTest; + +import javax.microedition.khronos.opengles.GL11; + +import junit.framework.TestCase; + +@SmallTest +public class TextureTest extends TestCase { + @SuppressWarnings("unused") + private static final String TAG = "TextureTest"; + + class MyBasicTexture extends BasicTexture { + int mOnBindCalled; + int mOpaqueCalled; + + MyBasicTexture(GLCanvas canvas, int id) { + super(canvas, id, BasicTexture.STATE_UNLOADED); + } + + @Override + protected boolean onBind(GLCanvas canvas) { + mOnBindCalled++; + return true; + } + + public boolean isOpaque() { + mOpaqueCalled++; + return true; + } + + void upload() { + mState = STATE_LOADED; + } + } + + @SmallTest + public void testBasicTexture() { + GL11 glStub = new GLStub(); + GLCanvas canvas = new GLCanvasImpl(glStub); + MyBasicTexture texture = new MyBasicTexture(canvas, 47); + + assertEquals(47, texture.getId()); + texture.setSize(1, 1); + assertEquals(1, texture.getWidth()); + assertEquals(1, texture.getHeight()); + assertEquals(1, texture.getTextureWidth()); + assertEquals(1, texture.getTextureHeight()); + texture.setSize(3, 5); + assertEquals(3, texture.getWidth()); + assertEquals(5, texture.getHeight()); + assertEquals(4, texture.getTextureWidth()); + assertEquals(8, texture.getTextureHeight()); + + assertFalse(texture.isLoaded(canvas)); + texture.upload(); + assertTrue(texture.isLoaded(canvas)); + + // For a different GL, it's not loaded. + GLCanvas canvas2 = new GLCanvasImpl(new GLStub()); + assertFalse(texture.isLoaded(canvas2)); + + assertEquals(0, texture.mOnBindCalled); + assertEquals(0, texture.mOpaqueCalled); + texture.draw(canvas, 100, 200, 1, 1); + assertEquals(1, texture.mOnBindCalled); + assertEquals(1, texture.mOpaqueCalled); + texture.draw(canvas, 0, 0); + assertEquals(2, texture.mOnBindCalled); + assertEquals(2, texture.mOpaqueCalled); + } + + @SmallTest + public void testRawTexture() { + GL11 glStub = new GLStub(); + GLCanvas canvas = new GLCanvasImpl(glStub); + RawTexture texture = RawTexture.newInstance(canvas); + texture.onBind(canvas); + + GLCanvas canvas2 = new GLCanvasImpl(new GLStub()); + try { + texture.onBind(canvas2); + fail(); + } catch (RuntimeException ex) { + // expected. + } + + assertTrue(texture.isOpaque()); + } + + @SmallTest + public void testColorTexture() { + GLCanvasMock canvas = new GLCanvasMock(); + ColorTexture texture = new ColorTexture(0x12345678); + + texture.setSize(42, 47); + assertEquals(texture.getWidth(), 42); + assertEquals(texture.getHeight(), 47); + assertEquals(0, canvas.mFillRectCalled); + texture.draw(canvas, 0, 0); + assertEquals(1, canvas.mFillRectCalled); + assertEquals(0x12345678, canvas.mFillRectColor); + assertEquals(42f, canvas.mFillRectWidth); + assertEquals(47f, canvas.mFillRectHeight); + assertFalse(texture.isOpaque()); + assertTrue(new ColorTexture(0xFF000000).isOpaque()); + } + + private class MyUploadedTexture extends UploadedTexture { + int mGetCalled; + int mFreeCalled; + Bitmap mBitmap; + @Override + protected Bitmap onGetBitmap() { + mGetCalled++; + Config config = Config.ARGB_8888; + mBitmap = Bitmap.createBitmap(47, 42, config); + return mBitmap; + } + @Override + protected void onFreeBitmap(Bitmap bitmap) { + mFreeCalled++; + assertSame(mBitmap, bitmap); + mBitmap.recycle(); + mBitmap = null; + } + } + + @SmallTest + public void testUploadedTexture() { + GL11 glStub = new GLStub(); + GLCanvas canvas = new GLCanvasImpl(glStub); + MyUploadedTexture texture = new MyUploadedTexture(); + + // draw it and the bitmap should be fetched. + assertEquals(0, texture.mFreeCalled); + assertEquals(0, texture.mGetCalled); + texture.draw(canvas, 0, 0); + assertEquals(1, texture.mGetCalled); + assertTrue(texture.isLoaded(canvas)); + assertTrue(texture.isContentValid(canvas)); + + // invalidate content and it should be freed. + texture.invalidateContent(); + assertFalse(texture.isContentValid(canvas)); + assertEquals(1, texture.mFreeCalled); + assertTrue(texture.isLoaded(canvas)); // But it's still loaded + + // draw it again and the bitmap should be fetched again. + texture.draw(canvas, 0, 0); + assertEquals(2, texture.mGetCalled); + assertTrue(texture.isLoaded(canvas)); + assertTrue(texture.isContentValid(canvas)); + + // recycle the texture and it should be freed again. + texture.recycle(); + assertEquals(2, texture.mFreeCalled); + // TODO: these two are broken and waiting for fix. + //assertFalse(texture.isLoaded(canvas)); + //assertFalse(texture.isContentValid(canvas)); + } + + class MyTextureForMixed extends BasicTexture { + MyTextureForMixed(GLCanvas canvas, int id) { + super(canvas, id, BasicTexture.STATE_UNLOADED); + } + + @Override + protected boolean onBind(GLCanvas canvas) { + return true; + } + + public boolean isOpaque() { + return true; + } + } + + @SmallTest + public void testBitmapTexture() { + Config config = Config.ARGB_8888; + Bitmap bitmap = Bitmap.createBitmap(47, 42, config); + assertFalse(bitmap.isRecycled()); + BitmapTexture texture = new BitmapTexture(bitmap); + texture.recycle(); + assertFalse(bitmap.isRecycled()); + bitmap.recycle(); + assertTrue(bitmap.isRecycled()); + } +} diff --git a/tests/src/com/android/gallery3d/util/IntArrayTest.java b/tests/src/com/android/gallery3d/util/IntArrayTest.java new file mode 100644 index 000000000..83e605006 --- /dev/null +++ b/tests/src/com/android/gallery3d/util/IntArrayTest.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; + +import com.android.gallery3d.util.IntArray; + +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; + +import java.util.Arrays; +import junit.framework.TestCase; + +@SmallTest +public class IntArrayTest extends TestCase { + private static final String TAG = "IntArrayTest"; + + public void testIntArray() { + IntArray a = new IntArray(); + assertEquals(0, a.size()); + assertTrue(Arrays.equals(new int[] {}, a.toArray(null))); + + a.add(0); + assertEquals(1, a.size()); + assertTrue(Arrays.equals(new int[] {0}, a.toArray(null))); + + a.add(1); + assertEquals(2, a.size()); + assertTrue(Arrays.equals(new int[] {0, 1}, a.toArray(null))); + + int[] buf = new int[2]; + int[] result = a.toArray(buf); + assertSame(buf, result); + + IntArray b = new IntArray(); + for (int i = 0; i < 100; i++) { + b.add(i * i); + } + + assertEquals(100, b.size()); + result = b.toArray(buf); + assertEquals(100, result.length); + for (int i = 0; i < 100; i++) { + assertEquals(i * i, result[i]); + } + } +} |