blob: 7a7767b6d7980a991f8ac7482d76c2a78e8bc837 [file] [log] [blame]
/*
* 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.camera;
import com.android.camera.gallery.BaseCancelable;
import com.android.camera.gallery.BaseImageList;
import com.android.camera.gallery.Cancelable;
import com.android.camera.gallery.DrmImageList;
import com.android.camera.gallery.IImage;
import com.android.camera.gallery.IImageList;
import com.android.camera.gallery.Image;
import com.android.camera.gallery.ImageList;
import com.android.camera.gallery.ImageListUber;
import com.android.camera.gallery.SingleImageList;
import com.android.camera.gallery.VideoList;
import com.android.camera.gallery.VideoObject;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.location.Location;
import android.net.Uri;
import android.os.Environment;
import android.os.Parcel;
import android.provider.DrmStore;
import android.provider.MediaStore;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.Images.ImageColumns;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.concurrent.ExecutionException;
/**
* ImageManager is used to retrieve and store images
* in the media content provider.
*/
public class ImageManager {
private static final String TAG = "ImageManager";
private static final Uri STORAGE_URI = Images.Media.EXTERNAL_CONTENT_URI;
private static final Uri THUMB_URI
= Images.Thumbnails.EXTERNAL_CONTENT_URI;
private static final Uri VIDEO_STORAGE_URI =
Uri.parse("content://media/external/video/media");
/**
* Enumerate type for the location of the images in gallery.
*/
public static enum DataLocation { NONE, INTERNAL, EXTERNAL, ALL }
public static final Bitmap DEFAULT_THUMBNAIL =
Bitmap.createBitmap(32, 32, Bitmap.Config.RGB_565);
public static final Bitmap NO_IMAGE_BITMAP =
Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565);
public static final int SORT_ASCENDING = 1;
public static final int SORT_DESCENDING = 2;
public static final int INCLUDE_IMAGES = (1 << 0);
public static final int INCLUDE_DRM_IMAGES = (1 << 1);
public static final int INCLUDE_VIDEOS = (1 << 2);
public static final String CAMERA_IMAGE_BUCKET_NAME =
Environment.getExternalStorageDirectory().toString()
+ "/DCIM/Camera";
public static final String CAMERA_IMAGE_BUCKET_ID =
getBucketId(CAMERA_IMAGE_BUCKET_NAME);
/**
* Matches code in MediaProvider.computeBucketValues. Should be a common
* function.
*/
public static String getBucketId(String path) {
return String.valueOf(path.toLowerCase().hashCode());
}
/**
* OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be
* imported. This is a temporary fix for bug#1655552.
*/
public static void ensureOSXCompatibleFolder() {
File nnnAAAAA = new File(
Environment.getExternalStorageDirectory().toString()
+ "/DCIM/100ANDRO");
if ((!nnnAAAAA.exists()) && (!nnnAAAAA.mkdir())) {
Log.e(TAG, "create NNNAAAAA file: " + nnnAAAAA.getPath()
+ " failed");
}
}
public static DataLocation getDefaultDataLocation() {
return DataLocation.EXTERNAL;
}
public static int roundOrientation(int orientationInput) {
int orientation = orientationInput;
if (orientation == -1) {
orientation = 0;
}
orientation = orientation % 360;
int retVal;
if (orientation < (0 * 90) + 45) {
retVal = 0;
} else if (orientation < (1 * 90) + 45) {
retVal = 90;
} else if (orientation < (2 * 90) + 45) {
retVal = 180;
} else if (orientation < (3 * 90) + 45) {
retVal = 270;
} else {
retVal = 0;
}
return retVal;
}
/**
* @return true if the mimetype is an image mimetype.
*/
public static boolean isImageMimeType(String mimeType) {
return mimeType.startsWith("image/");
}
/**
* @return true if the image is an image.
*/
public static boolean isImage(IImage image) {
return isImageMimeType(image.getMimeType());
}
/**
* @return true if the image is a video.
*/
public static boolean isVideo(IImage image) {
// This is the right implementation, but we use instanceof for speed.
//return Util.isVideoMimeType(image.getMimeType());
return (image instanceof VideoObject);
}
public static void setImageSize(ContentResolver cr, Uri uri, long size) {
ContentValues values = new ContentValues();
values.put(Images.Media.SIZE, size);
cr.update(uri, values, null, null);
}
public static Uri addImage(ContentResolver cr, String title,
long dateTaken, Location location,
int orientation, String directory, String filename) {
ContentValues values = new ContentValues(7);
values.put(Images.Media.TITLE, title);
// That filename is what will be handed to Gmail when a user shares a
// photo. Gmail gets the name of the picture attachment from the
// "DISPLAY_NAME" field.
values.put(Images.Media.DISPLAY_NAME, filename);
values.put(Images.Media.DATE_TAKEN, dateTaken);
values.put(Images.Media.MIME_TYPE, "image/jpeg");
values.put(Images.Media.ORIENTATION, orientation);
if (location != null) {
values.put(Images.Media.LATITUDE, location.getLatitude());
values.put(Images.Media.LONGITUDE, location.getLongitude());
}
if (directory != null && filename != null) {
String value = directory + "/" + filename;
values.put(Images.Media.DATA, value);
}
return cr.insert(STORAGE_URI, values);
}
private static class AddImageCancelable extends BaseCancelable<Void> {
private final Uri mUri;
private final ContentResolver mCr;
private final int mOrientation;
private final Bitmap mSource;
private final byte [] mJpegData;
public AddImageCancelable(Uri uri, ContentResolver cr,
int orientation, Bitmap source, byte[] jpegData) {
if (source == null && jpegData == null || uri == null) {
throw new IllegalArgumentException("source cannot be null");
}
mUri = uri;
mCr = cr;
mOrientation = orientation;
mSource = source;
mJpegData = jpegData;
}
@Override
protected Void execute() throws InterruptedException,
ExecutionException {
boolean complete = false;
try {
long id = ContentUris.parseId(mUri);
BaseImageList il = new ImageList(
STORAGE_URI, THUMB_URI, SORT_ASCENDING, null);
il.open(mCr);
// TODO: Redesign the process of adding new images. We should
// create an <code>IImage</code> in "ImageManager.addImage"
// and pass the image object to here.
Image image = new Image(il, mCr, id, 0, il.contentUri(id), null,
0, null, 0, null, null, 0);
String[] projection = new String[] {
ImageColumns._ID,
ImageColumns.MINI_THUMB_MAGIC, ImageColumns.DATA};
Cursor c = mCr.query(mUri, projection, null, null, null);
String filepath;
try {
c.moveToPosition(0);
filepath = c.getString(2);
} finally {
c.close();
}
runSubTask(image.saveImageContents(
mSource, mJpegData, mOrientation, true, filepath));
ContentValues values = new ContentValues();
values.put(ImageColumns.MINI_THUMB_MAGIC, 0);
mCr.update(mUri, values, null, null);
complete = true;
return null;
} finally {
if (!complete) {
try {
mCr.delete(mUri, null, null);
} catch (Throwable t) {
// ignore it while clean up.
}
}
}
}
}
public static Cancelable<Void> storeImage(
Uri uri, ContentResolver cr, int orientation,
Bitmap source, byte [] jpegData) {
return new AddImageCancelable(
uri, cr, orientation, source, jpegData);
}
public static IImageList makeImageList(Uri uri, ContentResolver cr,
int sort) {
String uriString = (uri != null) ? uri.toString() : "";
// TODO: we need to figure out whether we're viewing
// DRM images in a better way. Is there a constant
// for content://drm somewhere??
IImageList imageList;
if (uriString.startsWith("content://drm")) {
imageList = ImageManager.allImages(
cr, ImageManager.DataLocation.ALL,
ImageManager.INCLUDE_DRM_IMAGES, sort);
} else if (uriString.startsWith("content://media/external/video")) {
imageList = ImageManager.allImages(
cr, ImageManager.DataLocation.EXTERNAL,
ImageManager.INCLUDE_VIDEOS, sort);
} else if (isSingleImageMode(uriString)) {
imageList = new SingleImageList(uri);
((SingleImageList) imageList).open(cr);
} else {
String bucketId = uri.getQueryParameter("bucketId");
imageList = ImageManager.allImages(
cr, ImageManager.DataLocation.ALL,
ImageManager.INCLUDE_IMAGES, sort, bucketId);
}
return imageList;
}
private static boolean isSingleImageMode(String uriString) {
return !uriString.startsWith(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())
&& !uriString.startsWith(
MediaStore.Images.Media.INTERNAL_CONTENT_URI.toString());
}
private static class EmptyImageList implements IImageList {
public static final Creator<EmptyImageList> CREATOR =
new Creator<EmptyImageList>() {
public EmptyImageList createFromParcel(Parcel in) {
return new EmptyImageList();
}
public EmptyImageList[] newArray(int size) {
return new EmptyImageList[size];
}
};
public void open(ContentResolver resolver) {
}
public void close() {
}
public void checkThumbnail(int index) {
}
public void deactivate() {
}
public HashMap<String, String> getBucketIds() {
return new HashMap<String, String>();
}
public int getCount() {
return 0;
}
public boolean isEmpty() {
return true;
}
public IImage getImageAt(int i) {
return null;
}
public IImage getImageForUri(Uri uri) {
return null;
}
public boolean removeImage(IImage image) {
return false;
}
public boolean removeImageAt(int i) {
return false;
}
public int getImageIndex(IImage image) {
throw new UnsupportedOperationException();
}
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel dest, int flags) {
}
}
public static IImageList emptyImageList() {
return new EmptyImageList();
}
public static IImageList allImages(ContentResolver cr,
DataLocation location, int inclusion, int sort) {
return allImages(cr, location, inclusion, sort, null);
}
public static IImageList allImages(ContentResolver cr,
DataLocation location, int inclusion, int sort, String bucketId) {
if (cr == null) {
return null;
}
// false ==> don't require write access
boolean haveSdCard = hasStorage(false);
// use this code to merge videos and stills into the same list
ArrayList<BaseImageList> l = new ArrayList<BaseImageList>();
if (haveSdCard && location != DataLocation.INTERNAL) {
if ((inclusion & INCLUDE_IMAGES) != 0) {
l.add(new ImageList(
STORAGE_URI, THUMB_URI, sort, bucketId));
}
if ((inclusion & INCLUDE_VIDEOS) != 0) {
l.add(new VideoList(VIDEO_STORAGE_URI, sort, bucketId));
}
}
if (location == DataLocation.INTERNAL || location == DataLocation.ALL) {
if ((inclusion & INCLUDE_IMAGES) != 0) {
l.add(new ImageList(
Images.Media.INTERNAL_CONTENT_URI,
Images.Thumbnails.INTERNAL_CONTENT_URI,
sort, bucketId));
}
if ((inclusion & INCLUDE_DRM_IMAGES) != 0) {
l.add(new DrmImageList(
DrmStore.Images.CONTENT_URI, sort, bucketId));
}
}
// Optimization: If some of the lists are empty, remove them.
// If there is only one remaining list, return it directly.
Iterator<BaseImageList> iter = l.iterator();
while (iter.hasNext()) {
BaseImageList sublist = iter.next();
sublist.open(cr);
if (sublist.isEmpty()) iter.remove();
sublist.close();
}
if (l.size() == 1) {
BaseImageList list = l.get(0);
list.open(cr);
return list;
}
ImageListUber uber = new ImageListUber(
l.toArray(new IImageList[l.size()]), sort);
uber.open(cr);
return uber;
}
private static boolean checkFsWritable() {
// Create a temporary file to see whether a volume is really writeable.
// It's important not to put it in the root directory which may have a
// limit on the number of files.
String directoryName =
Environment.getExternalStorageDirectory().toString() + "/DCIM";
File directory = new File(directoryName);
if (!directory.isDirectory()) {
if (!directory.mkdirs()) {
return false;
}
}
File f = new File(directoryName, ".probe");
try {
// Remove stale file if any
if (f.exists()) {
f.delete();
}
if (!f.createNewFile()) {
return false;
}
f.delete();
return true;
} catch (IOException ex) {
return false;
}
}
public static boolean hasStorage() {
return hasStorage(true);
}
public static boolean hasStorage(boolean requireWriteAccess) {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
if (requireWriteAccess) {
boolean writable = checkFsWritable();
return writable;
} else {
return true;
}
} else if (!requireWriteAccess
&& Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
return true;
}
return false;
}
private static Cursor query(ContentResolver resolver, Uri uri,
String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
try {
if (resolver == null) {
return null;
}
return resolver.query(
uri, projection, selection, selectionArgs, sortOrder);
} catch (UnsupportedOperationException ex) {
return null;
}
}
public static boolean isMediaScannerScanning(ContentResolver cr) {
boolean result = false;
Cursor cursor = query(cr, MediaStore.getMediaScannerUri(),
new String [] {MediaStore.MEDIA_SCANNER_VOLUME},
null, null, null);
if (cursor != null) {
if (cursor.getCount() == 1) {
cursor.moveToFirst();
result = "external".equals(cursor.getString(0));
}
cursor.close();
}
return result;
}
public static String getLastImageThumbPath() {
return Environment.getExternalStorageDirectory().toString() +
"/DCIM/.thumbnails/image_last_thumb";
}
public static String getLastVideoThumbPath() {
return Environment.getExternalStorageDirectory().toString() +
"/DCIM/.thumbnails/video_last_thumb";
}
}