diff options
33 files changed, 1053 insertions, 291 deletions
diff --git a/res/layout/album_set_item.xml b/res/layout/album_set_item.xml index 46084e938..bdecd5fd1 100644 --- a/res/layout/album_set_item.xml +++ b/res/layout/album_set_item.xml @@ -12,6 +12,8 @@ android:layout_alignParentTop="true" android:layout_marginLeft="10dp" android:layout_marginTop="10dp" + android:ellipsize="end" + android:singleLine="true" android:textAppearance="?android:attr/textAppearanceMedium" /> <TextView diff --git a/res/layout/photo_set.xml b/res/layout/photo_set.xml index f6ff637d6..d929cadfb 100644 --- a/res/layout/photo_set.xml +++ b/res/layout/photo_set.xml @@ -5,12 +5,18 @@ android:paddingLeft="8dp" android:paddingRight="8dp" > - <com.android.photos.views.GalleryThumbnailView + <GridView android:id="@id/android:list" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" - android:drawSelectorOnTop="true" /> + android:drawSelectorOnTop="true" + android:numColumns="auto_fit" + android:stretchMode="columnWidth" + android:columnWidth="200dip" + android:horizontalSpacing="4dip" + android:verticalSpacing="4dip" + android:padding="4dip" /> <TextView android:id="@id/android:empty" diff --git a/res/layout/photo_set_item.xml b/res/layout/photo_set_item.xml new file mode 100644 index 000000000..b56184e59 --- /dev/null +++ b/res/layout/photo_set_item.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<ImageView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="200dip" + android:id="@+id/thumbnail"> + +</ImageView>
\ No newline at end of file diff --git a/res/values-be/filtershow_strings.xml b/res/values-be/filtershow_strings.xml index 3a83c23b9..fd1caee53 100644 --- a/res/values-be/filtershow_strings.xml +++ b/res/values-be/filtershow_strings.xml @@ -34,8 +34,7 @@ <string name="reset" msgid="9013181350779592937">"Скінуць"</string> <!-- no translation found for history_original (150973253194312841) --> <skip /> - <!-- no translation found for imageState (8632586742752891968) --> - <skip /> + <string name="imageState" msgid="8632586742752891968">"Прымененыя эфекты"</string> <string name="compare_original" msgid="8140838959007796977">"Параўнаць"</string> <string name="apply_effect" msgid="1218288221200568947">"Паспрабаваць"</string> <string name="reset_effect" msgid="7712605581024929564">"Скінуць"</string> diff --git a/res/values-et/filtershow_strings.xml b/res/values-et/filtershow_strings.xml index 01225b86c..161ac9048 100644 --- a/res/values-et/filtershow_strings.xml +++ b/res/values-et/filtershow_strings.xml @@ -34,8 +34,7 @@ <string name="reset" msgid="9013181350779592937">"Lähtesta"</string> <!-- no translation found for history_original (150973253194312841) --> <skip /> - <!-- no translation found for imageState (8632586742752891968) --> - <skip /> + <string name="imageState" msgid="8632586742752891968">"Rakendatud efektid"</string> <string name="compare_original" msgid="8140838959007796977">"Võrdle"</string> <string name="apply_effect" msgid="1218288221200568947">"Rakenda"</string> <string name="reset_effect" msgid="7712605581024929564">"Lähtesta"</string> diff --git a/res/values-fr/filtershow_strings.xml b/res/values-fr/filtershow_strings.xml index edeafa061..24685c02c 100644 --- a/res/values-fr/filtershow_strings.xml +++ b/res/values-fr/filtershow_strings.xml @@ -34,8 +34,7 @@ <string name="reset" msgid="9013181350779592937">"Réinitialiser"</string> <!-- no translation found for history_original (150973253194312841) --> <skip /> - <!-- no translation found for imageState (8632586742752891968) --> - <skip /> + <string name="imageState" msgid="8632586742752891968">"Effets appliqués"</string> <string name="compare_original" msgid="8140838959007796977">"Comparer"</string> <string name="apply_effect" msgid="1218288221200568947">"Appliquer"</string> <string name="reset_effect" msgid="7712605581024929564">"Réinitialiser"</string> diff --git a/res/values-ko/filtershow_strings.xml b/res/values-ko/filtershow_strings.xml index 7eaa641ed..902649f48 100644 --- a/res/values-ko/filtershow_strings.xml +++ b/res/values-ko/filtershow_strings.xml @@ -34,8 +34,7 @@ <string name="reset" msgid="9013181350779592937">"초기화"</string> <!-- no translation found for history_original (150973253194312841) --> <skip /> - <!-- no translation found for imageState (8632586742752891968) --> - <skip /> + <string name="imageState" msgid="8632586742752891968">"적용된 효과"</string> <string name="compare_original" msgid="8140838959007796977">"비교하기"</string> <string name="apply_effect" msgid="1218288221200568947">"적용"</string> <string name="reset_effect" msgid="7712605581024929564">"초기화"</string> diff --git a/res/values-lt/filtershow_strings.xml b/res/values-lt/filtershow_strings.xml index 54407b16e..2f91fb629 100644 --- a/res/values-lt/filtershow_strings.xml +++ b/res/values-lt/filtershow_strings.xml @@ -34,8 +34,7 @@ <string name="reset" msgid="9013181350779592937">"Nust. iš naujo"</string> <!-- no translation found for history_original (150973253194312841) --> <skip /> - <!-- no translation found for imageState (8632586742752891968) --> - <skip /> + <string name="imageState" msgid="8632586742752891968">"Pritaikyti efektai"</string> <string name="compare_original" msgid="8140838959007796977">"Palyginti"</string> <string name="apply_effect" msgid="1218288221200568947">"Taikyti"</string> <string name="reset_effect" msgid="7712605581024929564">"Nust. iš naujo"</string> diff --git a/res/values-lv/filtershow_strings.xml b/res/values-lv/filtershow_strings.xml index 7fbfb8c42..155784c49 100644 --- a/res/values-lv/filtershow_strings.xml +++ b/res/values-lv/filtershow_strings.xml @@ -34,8 +34,7 @@ <string name="reset" msgid="9013181350779592937">"Atiestatīt"</string> <!-- no translation found for history_original (150973253194312841) --> <skip /> - <!-- no translation found for imageState (8632586742752891968) --> - <skip /> + <string name="imageState" msgid="8632586742752891968">"Izmantotie efekti"</string> <string name="compare_original" msgid="8140838959007796977">"Salīdzināt"</string> <string name="apply_effect" msgid="1218288221200568947">"Lietot"</string> <string name="reset_effect" msgid="7712605581024929564">"Atiestatīt"</string> diff --git a/res/values-pl/filtershow_strings.xml b/res/values-pl/filtershow_strings.xml index 834677741..880cf824b 100644 --- a/res/values-pl/filtershow_strings.xml +++ b/res/values-pl/filtershow_strings.xml @@ -34,8 +34,7 @@ <string name="reset" msgid="9013181350779592937">"Resetuj"</string> <!-- no translation found for history_original (150973253194312841) --> <skip /> - <!-- no translation found for imageState (8632586742752891968) --> - <skip /> + <string name="imageState" msgid="8632586742752891968">"Zastosowane efekty"</string> <string name="compare_original" msgid="8140838959007796977">"Porównaj"</string> <string name="apply_effect" msgid="1218288221200568947">"Zastosuj"</string> <string name="reset_effect" msgid="7712605581024929564">"Resetuj"</string> diff --git a/res/values-pt/filtershow_strings.xml b/res/values-pt/filtershow_strings.xml index d5ba68138..14a7520a2 100644 --- a/res/values-pt/filtershow_strings.xml +++ b/res/values-pt/filtershow_strings.xml @@ -34,8 +34,7 @@ <string name="reset" msgid="9013181350779592937">"Restaurar"</string> <!-- no translation found for history_original (150973253194312841) --> <skip /> - <!-- no translation found for imageState (8632586742752891968) --> - <skip /> + <string name="imageState" msgid="8632586742752891968">"Efeitos aplicados"</string> <string name="compare_original" msgid="8140838959007796977">"Comparar"</string> <string name="apply_effect" msgid="1218288221200568947">"Aplicar"</string> <string name="reset_effect" msgid="7712605581024929564">"Restaurar"</string> diff --git a/res/values-ro/filtershow_strings.xml b/res/values-ro/filtershow_strings.xml index f59b3d011..68b067cc9 100644 --- a/res/values-ro/filtershow_strings.xml +++ b/res/values-ro/filtershow_strings.xml @@ -34,8 +34,7 @@ <string name="reset" msgid="9013181350779592937">"Resetaţi"</string> <!-- no translation found for history_original (150973253194312841) --> <skip /> - <!-- no translation found for imageState (8632586742752891968) --> - <skip /> + <string name="imageState" msgid="8632586742752891968">"Efecte aplicate"</string> <string name="compare_original" msgid="8140838959007796977">"Comparaţi"</string> <string name="apply_effect" msgid="1218288221200568947">"Aplicaţi"</string> <string name="reset_effect" msgid="7712605581024929564">"Resetaţi"</string> diff --git a/res/values-ru/filtershow_strings.xml b/res/values-ru/filtershow_strings.xml index ad9f545c4..cb18bedaa 100644 --- a/res/values-ru/filtershow_strings.xml +++ b/res/values-ru/filtershow_strings.xml @@ -34,8 +34,7 @@ <string name="reset" msgid="9013181350779592937">"Сброс"</string> <!-- no translation found for history_original (150973253194312841) --> <skip /> - <!-- no translation found for imageState (8632586742752891968) --> - <skip /> + <string name="imageState" msgid="8632586742752891968">"Эффекты"</string> <string name="compare_original" msgid="8140838959007796977">"Сравнить"</string> <string name="apply_effect" msgid="1218288221200568947">"Применить:"</string> <string name="reset_effect" msgid="7712605581024929564">"Сброс"</string> diff --git a/res/values-sk/filtershow_strings.xml b/res/values-sk/filtershow_strings.xml index 7a5e093ac..b999871b4 100644 --- a/res/values-sk/filtershow_strings.xml +++ b/res/values-sk/filtershow_strings.xml @@ -34,8 +34,7 @@ <string name="reset" msgid="9013181350779592937">"Obnoviť"</string> <!-- no translation found for history_original (150973253194312841) --> <skip /> - <!-- no translation found for imageState (8632586742752891968) --> - <skip /> + <string name="imageState" msgid="8632586742752891968">"Použité efekty"</string> <string name="compare_original" msgid="8140838959007796977">"Porovnať"</string> <string name="apply_effect" msgid="1218288221200568947">"Použiť"</string> <string name="reset_effect" msgid="7712605581024929564">"Obnoviť"</string> diff --git a/res/values-sv/filtershow_strings.xml b/res/values-sv/filtershow_strings.xml index 109db1320..3644a5ff5 100644 --- a/res/values-sv/filtershow_strings.xml +++ b/res/values-sv/filtershow_strings.xml @@ -34,8 +34,7 @@ <string name="reset" msgid="9013181350779592937">"Återställ"</string> <!-- no translation found for history_original (150973253194312841) --> <skip /> - <!-- no translation found for imageState (8632586742752891968) --> - <skip /> + <string name="imageState" msgid="8632586742752891968">"Effekter som används"</string> <string name="compare_original" msgid="8140838959007796977">"Jämför"</string> <string name="apply_effect" msgid="1218288221200568947">"Använd"</string> <string name="reset_effect" msgid="7712605581024929564">"Återställ"</string> diff --git a/res/values-sw/filtershow_strings.xml b/res/values-sw/filtershow_strings.xml index 93db6df69..6f66386b1 100644 --- a/res/values-sw/filtershow_strings.xml +++ b/res/values-sw/filtershow_strings.xml @@ -34,8 +34,7 @@ <string name="reset" msgid="9013181350779592937">"Weka upya"</string> <!-- no translation found for history_original (150973253194312841) --> <skip /> - <!-- no translation found for imageState (8632586742752891968) --> - <skip /> + <string name="imageState" msgid="8632586742752891968">"Madoido Yanayotumiwa"</string> <string name="compare_original" msgid="8140838959007796977">"Linganisha"</string> <string name="apply_effect" msgid="1218288221200568947">"Tekeleza"</string> <string name="reset_effect" msgid="7712605581024929564">"Weka upya"</string> diff --git a/res/values-tr/filtershow_strings.xml b/res/values-tr/filtershow_strings.xml index 2566ff450..63740ec27 100644 --- a/res/values-tr/filtershow_strings.xml +++ b/res/values-tr/filtershow_strings.xml @@ -34,8 +34,7 @@ <string name="reset" msgid="9013181350779592937">"Sıfırla"</string> <!-- no translation found for history_original (150973253194312841) --> <skip /> - <!-- no translation found for imageState (8632586742752891968) --> - <skip /> + <string name="imageState" msgid="8632586742752891968">"Uygulanan Efektler"</string> <string name="compare_original" msgid="8140838959007796977">"Karşılaştır"</string> <string name="apply_effect" msgid="1218288221200568947">"Uygula"</string> <string name="reset_effect" msgid="7712605581024929564">"Sıfırla"</string> diff --git a/res/values-zh-rCN/filtershow_strings.xml b/res/values-zh-rCN/filtershow_strings.xml index 26b14ed0b..1c060bc57 100644 --- a/res/values-zh-rCN/filtershow_strings.xml +++ b/res/values-zh-rCN/filtershow_strings.xml @@ -34,8 +34,7 @@ <string name="reset" msgid="9013181350779592937">"重置"</string> <!-- no translation found for history_original (150973253194312841) --> <skip /> - <!-- no translation found for imageState (8632586742752891968) --> - <skip /> + <string name="imageState" msgid="8632586742752891968">"运用的效果"</string> <string name="compare_original" msgid="8140838959007796977">"比较"</string> <string name="apply_effect" msgid="1218288221200568947">"应用"</string> <string name="reset_effect" msgid="7712605581024929564">"重置"</string> diff --git a/src/com/android/photos/AlbumSetFragment.java b/src/com/android/photos/AlbumSetFragment.java index 6a9520a5e..3c51bbac3 100644 --- a/src/com/android/photos/AlbumSetFragment.java +++ b/src/com/android/photos/AlbumSetFragment.java @@ -17,14 +17,15 @@ package com.android.photos; import android.app.Fragment; +import android.app.LoaderManager.LoaderCallbacks; import android.content.Context; +import android.content.Loader; import android.database.Cursor; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.format.DateFormat; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; @@ -37,15 +38,20 @@ import android.widget.Toast; import com.android.gallery3d.R; import com.android.photos.data.AlbumSetLoader; -import com.android.photos.drawables.DataUriThumbnailDrawable; +import com.android.photos.drawables.DrawableFactory; +import com.android.photos.shims.MediaSetLoader; import java.util.Date; -public class AlbumSetFragment extends Fragment implements OnItemClickListener { +public class AlbumSetFragment extends Fragment implements OnItemClickListener, + LoaderCallbacks<Cursor> { + private GridView mAlbumSetView; private View mEmptyView; - private CursorAdapter mAdapter; + private AlbumSetCursorAdapter mAdapter; + + private static final int LOADER_ALBUMSET = 1; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -57,11 +63,37 @@ public class AlbumSetFragment extends Fragment implements OnItemClickListener { mAdapter = new AlbumSetCursorAdapter(getActivity()); mAlbumSetView.setAdapter(mAdapter); mAlbumSetView.setOnItemClickListener(this); - mAdapter.swapCursor(AlbumSetLoader.MOCK); + getLoaderManager().initLoader(LOADER_ALBUMSET, null, this); + updateEmptyStatus(); return root; } @Override + public Loader<Cursor> onCreateLoader(int id, Bundle args) { + // TODO: Switch to AlbumSetLoader + MediaSetLoader loader = new MediaSetLoader(getActivity()); + mAdapter.setDrawableFactory(loader); + return loader; + } + + @Override + public void onLoadFinished(Loader<Cursor> loader, + Cursor data) { + mAdapter.swapCursor(data); + updateEmptyStatus(); + } + + private void updateEmptyStatus() { + boolean empty = (mAdapter == null || mAdapter.getCount() == 0); + mAlbumSetView.setVisibility(empty ? View.GONE : View.VISIBLE); + mEmptyView.setVisibility(empty ? View.VISIBLE : View.GONE); + } + + @Override + public void onLoaderReset(Loader<Cursor> loader) { + } + + @Override public void onItemClick(AdapterView<?> av, View v, int pos, long id) { Cursor c = (Cursor) av.getItemAtPosition(pos); int albumId = c.getInt(AlbumSetLoader.INDEX_ID); @@ -71,6 +103,11 @@ public class AlbumSetFragment extends Fragment implements OnItemClickListener { private static class AlbumSetCursorAdapter extends CursorAdapter { + private DrawableFactory<Cursor> mDrawableFactory; + + public void setDrawableFactory(DrawableFactory<Cursor> factory) { + mDrawableFactory = factory; + } private Date mDate = new Date(); // Used for converting timestamps for display public AlbumSetCursorAdapter(Context context) { @@ -85,8 +122,13 @@ public class AlbumSetFragment extends Fragment implements OnItemClickListener { TextView dateTextView = (TextView) v.findViewById( R.id.album_set_item_date); - mDate.setTime(cursor.getLong(AlbumSetLoader.INDEX_TIMESTAMP)); - dateTextView.setText(DateFormat.getMediumDateFormat(context).format(mDate)); + long timestamp = cursor.getLong(AlbumSetLoader.INDEX_TIMESTAMP); + if (timestamp > 0) { + mDate.setTime(timestamp); + dateTextView.setText(DateFormat.getMediumDateFormat(context).format(mDate)); + } else { + dateTextView.setText(null); + } ProgressBar uploadProgressBar = (ProgressBar) v.findViewById( R.id.album_set_item_upload_progress); @@ -97,17 +139,19 @@ public class AlbumSetFragment extends Fragment implements OnItemClickListener { uploadProgressBar.setVisibility(View.INVISIBLE); } - // TODO show the thumbnail + ImageView thumbImageView = (ImageView) v.findViewById( + R.id.album_set_item_image); + Drawable recycle = thumbImageView.getDrawable(); + Drawable drawable = mDrawableFactory.drawableForItem(cursor, recycle); + if (recycle != drawable) { + thumbImageView.setImageDrawable(drawable); + } } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { - View v = LayoutInflater.from(context).inflate( + return LayoutInflater.from(context).inflate( R.layout.album_set_item, parent, false); - ImageView thumbImageView = (ImageView) v.findViewById( - R.id.album_set_item_image); - thumbImageView.setImageResource(android.R.color.darker_gray); - return v; } } } diff --git a/src/com/android/photos/PhotoSetFragment.java b/src/com/android/photos/PhotoSetFragment.java index 0e9efa4b1..1de8de5a7 100644 --- a/src/com/android/photos/PhotoSetFragment.java +++ b/src/com/android/photos/PhotoSetFragment.java @@ -19,22 +19,22 @@ package com.android.photos; import android.app.Fragment; import android.app.LoaderManager.LoaderCallbacks; import android.content.Context; -import android.content.Intent; import android.content.Loader; import android.database.Cursor; -import android.net.Uri; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; import android.widget.CursorAdapter; +import android.widget.GridView; import android.widget.ImageView; import com.android.gallery3d.R; import com.android.photos.data.PhotoSetLoader; -import com.android.photos.drawables.DataUriThumbnailDrawable; -import com.android.photos.views.GalleryThumbnailView; +import com.android.photos.drawables.DrawableFactory; +import com.android.photos.shims.MediaItemsLoader; import com.android.photos.views.GalleryThumbnailView.GalleryThumbnailAdapter; @@ -42,7 +42,7 @@ public class PhotoSetFragment extends Fragment implements LoaderCallbacks<Cursor private static final int LOADER_PHOTOSET = 1; - private GalleryThumbnailView mPhotoSetView; + private GridView mPhotoSetView; private View mEmptyView; private ThumbnailAdapter mAdapter; @@ -50,7 +50,9 @@ public class PhotoSetFragment extends Fragment implements LoaderCallbacks<Cursor public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.photo_set, container, false); - mPhotoSetView = (GalleryThumbnailView) root.findViewById(android.R.id.list); + mPhotoSetView = (GridView) root.findViewById(android.R.id.list); + // TODO: Remove once UI stabilizes + mPhotoSetView.setColumnWidth(MediaItemsLoader.getThumbnailSize()); mEmptyView = root.findViewById(android.R.id.empty); mEmptyView.setVisibility(View.GONE); mAdapter = new ThumbnailAdapter(getActivity()); @@ -68,7 +70,10 @@ public class PhotoSetFragment extends Fragment implements LoaderCallbacks<Cursor @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { - return new PhotoSetLoader(getActivity()); + // TODO: Switch to PhotoSetLoader + MediaItemsLoader loader = new MediaItemsLoader(getActivity()); + mAdapter.setDrawableFactory(loader); + return loader; } @Override @@ -82,46 +87,37 @@ public class PhotoSetFragment extends Fragment implements LoaderCallbacks<Cursor public void onLoaderReset(Loader<Cursor> loader) { } - private static class ShowFullScreen implements OnClickListener { - - @Override - public void onClick(View view) { - String path = (String) view.getTag(); - Intent intent = new Intent(view.getContext(), FullscreenViewer.class); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(path)); - view.getContext().startActivity(intent); - } - - } - private static class ThumbnailAdapter extends CursorAdapter implements GalleryThumbnailAdapter { - private static ShowFullScreen sShowFullscreenClickListener = new ShowFullScreen(); + private LayoutInflater mInflater; + private DrawableFactory<Cursor> mDrawableFactory; public ThumbnailAdapter(Context context) { super(context, null, false); + mInflater = LayoutInflater.from(context); + } + + public void setDrawableFactory(DrawableFactory<Cursor> factory) { + mDrawableFactory = factory; } @Override public void bindView(View view, Context context, Cursor cursor) { ImageView iv = (ImageView) view; - DataUriThumbnailDrawable drawable = (DataUriThumbnailDrawable) iv.getDrawable(); - int width = cursor.getInt(PhotoSetLoader.INDEX_WIDTH); - int height = cursor.getInt(PhotoSetLoader.INDEX_HEIGHT); - String path = cursor.getString(PhotoSetLoader.INDEX_DATA); - drawable.setImage(path, width, height); - iv.setTag(path); + Drawable recycle = iv.getDrawable(); + Drawable drawable = mDrawableFactory.drawableForItem(cursor, recycle); + if (recycle != drawable) { + iv.setImageDrawable(drawable); + } } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { - ImageView iv = new ImageView(context); - DataUriThumbnailDrawable drawable = new DataUriThumbnailDrawable(); - iv.setImageDrawable(drawable); - int padding = (int) Math.ceil(2 * context.getResources().getDisplayMetrics().density); - iv.setPadding(padding, padding, padding, padding); - iv.setOnClickListener(sShowFullscreenClickListener); - return iv; + View view = mInflater.inflate(R.layout.photo_set_item, parent, false); + LayoutParams params = view.getLayoutParams(); + int columnWidth = ((GridView) parent).getColumnWidth(); + params.height = columnWidth; + view.setLayoutParams(params); + return view; } @Override diff --git a/src/com/android/photos/data/AlbumSetLoader.java b/src/com/android/photos/data/AlbumSetLoader.java index f5fc3b732..b2b5204e6 100644 --- a/src/com/android/photos/data/AlbumSetLoader.java +++ b/src/com/android/photos/data/AlbumSetLoader.java @@ -13,20 +13,20 @@ public class AlbumSetLoader { public static final int INDEX_COUNT_PENDING_UPLOAD = 6; public static final int INDEX_COUNT = 7; + public static final String[] PROJECTION = { + "_id", + "title", + "timestamp", + "thumb_uri", + "thumb_width", + "thumb_height", + "count_pending_upload", + "_count" + }; public static final MatrixCursor MOCK = createRandomCursor(30); private static MatrixCursor createRandomCursor(int count) { - String[] rows = { - "_id", - "title", - "timestamp", - "thumb_uri", - "thumb_width", - "thumb_height", - "count_pending_upload", - "_count" - }; - MatrixCursor c = new MatrixCursor(rows, count); + MatrixCursor c = new MatrixCursor(PROJECTION, count); for (int i = 0; i < count; i++) { c.addRow(createRandomRow()); } diff --git a/src/com/android/photos/data/MediaSetLoader.java b/src/com/android/photos/data/MediaSetLoader.java deleted file mode 100644 index 4afb7d922..000000000 --- a/src/com/android/photos/data/MediaSetLoader.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.photos.data; - -import android.content.AsyncTaskLoader; -import android.content.Context; - -import com.android.gallery3d.data.ContentListener; -import com.android.gallery3d.data.DataManager; -import com.android.gallery3d.data.MediaSet; -import com.android.gallery3d.data.MediaSet.SyncListener; -import com.android.gallery3d.util.Future; - -/** - * Proof of concept, don't use - */ -public class MediaSetLoader extends AsyncTaskLoader<MediaSet> { - - private static final SyncListener sNullListener = new SyncListener() { - @Override - public void onSyncDone(MediaSet mediaSet, int resultCode) { - } - }; - - private MediaSet mMediaSet; - private Future<Integer> mSyncTask = null; - private ContentListener mObserver = new ContentListener() { - @Override - public void onContentDirty() { - onContentChanged(); - } - }; - - public MediaSetLoader(Context context, String path) { - super(context); - mMediaSet = DataManager.from(getContext()).getMediaSet(path); - } - - @Override - protected void onStartLoading() { - super.onStartLoading(); - mMediaSet.addContentListener(mObserver); - mSyncTask = mMediaSet.requestSync(sNullListener); - forceLoad(); - } - - @Override - protected boolean onCancelLoad() { - if (mSyncTask != null) { - mSyncTask.cancel(); - mSyncTask = null; - } - return super.onCancelLoad(); - } - - @Override - protected void onStopLoading() { - super.onStopLoading(); - cancelLoad(); - mMediaSet.removeContentListener(mObserver); - } - - @Override - protected void onReset() { - super.onReset(); - onStopLoading(); - } - - @Override - public MediaSet loadInBackground() { - mMediaSet.loadIfDirty(); - return mMediaSet; - } - -} diff --git a/src/com/android/photos/data/NotificationWatcher.java b/src/com/android/photos/data/NotificationWatcher.java index 8cf0e3c8f..9041c236f 100644 --- a/src/com/android/photos/data/NotificationWatcher.java +++ b/src/com/android/photos/data/NotificationWatcher.java @@ -19,8 +19,7 @@ import android.net.Uri; import com.android.photos.data.PhotoProvider.ChangeNotification; -import java.util.HashSet; -import java.util.Set; +import java.util.ArrayList; /** * Used for capturing notifications from PhotoProvider without relying on @@ -28,11 +27,13 @@ import java.util.Set; * ContentObservers, so PhotoProvider allows this alternative for testing. */ public class NotificationWatcher implements ChangeNotification { - private Set<Uri> mUris = new HashSet<Uri>(); + private ArrayList<Uri> mUris = new ArrayList<Uri>(); + private boolean mSyncToNetwork = false; @Override - public void notifyChange(Uri uri) { + public void notifyChange(Uri uri, boolean syncToNetwork) { mUris.add(uri); + mSyncToNetwork = mSyncToNetwork || syncToNetwork; } public boolean isNotified(Uri uri) { @@ -43,7 +44,12 @@ public class NotificationWatcher implements ChangeNotification { return mUris.size(); } + public boolean syncToNetwork() { + return mSyncToNetwork; + } + public void reset() { mUris.clear(); + mSyncToNetwork = false; } } diff --git a/src/com/android/photos/data/PhotoDatabase.java b/src/com/android/photos/data/PhotoDatabase.java index 8585edc04..a87f00bfa 100644 --- a/src/com/android/photos/data/PhotoDatabase.java +++ b/src/com/android/photos/data/PhotoDatabase.java @@ -59,14 +59,13 @@ public class PhotoDatabase extends SQLiteOpenHelper { { Albums.ACCOUNT_ID, "INTEGER NOT NULL" }, // Albums.PARENT_ID is a foreign key to Albums._ID { Albums.PARENT_ID, "INTEGER" }, - { Albums.NAME, "Text NOT NULL" }, { Albums.VISIBILITY, "INTEGER NOT NULL" }, { Albums.LOCATION_STRING, "TEXT" }, - { Albums.TITLE, "TEXT" }, + { Albums.TITLE, "TEXT NOT NULL" }, { Albums.SUMMARY, "TEXT" }, { Albums.DATE_PUBLISHED, "INTEGER" }, { Albums.DATE_MODIFIED, "INTEGER" }, - createUniqueConstraint(Albums.PARENT_ID, Albums.NAME), + createUniqueConstraint(Albums.PARENT_ID, Albums.TITLE), }; private static final String[][] CREATE_METADATA = { diff --git a/src/com/android/photos/data/PhotoProvider.java b/src/com/android/photos/data/PhotoProvider.java index 52ebd6eee..cecfe5ea4 100644 --- a/src/com/android/photos/data/PhotoProvider.java +++ b/src/com/android/photos/data/PhotoProvider.java @@ -15,8 +15,10 @@ */ package com.android.photos.data; -import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentUris; import android.content.ContentValues; +import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.DatabaseUtils; @@ -28,7 +30,6 @@ import android.net.Uri; import android.os.CancellationSignal; import android.provider.BaseColumns; -import java.util.ArrayList; import java.util.List; /** @@ -45,7 +46,7 @@ import java.util.List; * in the selection. The selection and selectionArgs are not used when updating * metadata. If the metadata values are null, the row will be deleted. */ -public class PhotoProvider extends ContentProvider { +public class PhotoProvider extends SQLiteContentProvider { @SuppressWarnings("unused") private static final String TAG = PhotoProvider.class.getSimpleName(); @@ -57,7 +58,7 @@ public class PhotoProvider extends ContentProvider { // Used to allow mocking out the change notification because // MockContextResolver disallows system-wide notification. public static interface ChangeNotification { - void notifyChange(Uri uri); + void notifyChange(Uri uri, boolean syncToNetwork); } /** @@ -130,8 +131,6 @@ public class PhotoProvider extends ContentProvider { public static final String ACCOUNT_ID = "account_id"; /** Parent directory or null if this is in the root. */ public static final String PARENT_ID = "parent_id"; - /** Column name for the name of the album. String value. */ - public static final String NAME = "name"; /** * Column name for the visibility level of the album. Can be any of the * VISIBILITY_* values. @@ -214,9 +213,10 @@ public class PhotoProvider extends ContentProvider { public static final String IMAGE_TYPE_QUERY_PARAMETER = "image_type"; // ImageCache.IMAGE_TYPE values - public static final int IMAGE_TYPE_THUMBNAIL = 1; - public static final int IMAGE_TYPE_PREVIEW = 2; - public static final int IMAGE_TYPE_ORIGINAL = 3; + public static final int IMAGE_TYPE_ALBUM_COVER = 1; + public static final int IMAGE_TYPE_THUMBNAIL = 2; + public static final int IMAGE_TYPE_PREVIEW = 3; + public static final int IMAGE_TYPE_ORIGINAL = 4; /** * Content URI for retrieving image paths. The @@ -224,8 +224,18 @@ public class PhotoProvider extends ContentProvider { */ public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE); - /** Foreign key to the photos._id. Long value. */ - public static final String PHOTO_ID = "photo_id"; + /** + * Content URI for retrieving the album cover art. The album ID must be + * appended to the URI. + */ + public static final Uri ALBUM_COVER_CONTENT_URI = Uri.withAppendedPath(CONTENT_URI, + Albums.TABLE); + + /** + * An _ID from Albums or Photos, depending on whether IMAGE_TYPE is + * IMAGE_TYPE_ALBUM or not. Long value. + */ + public static final String REMOTE_ID = "remote_id"; /** One of IMAGE_TYPE_* values. */ public static final String IMAGE_TYPE = "image_type"; /** The String path to the image. */ @@ -262,7 +272,6 @@ public class PhotoProvider extends ContentProvider { }; protected ChangeNotification mNotifier = null; - private SQLiteOpenHelper mOpenHelper; protected static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); protected static final int MATCH_PHOTO = 1; @@ -272,6 +281,7 @@ public class PhotoProvider extends ContentProvider { protected static final int MATCH_METADATA = 5; protected static final int MATCH_METADATA_ID = 6; protected static final int MATCH_IMAGE = 7; + protected static final int MATCH_ALBUM_COVER = 8; static { sUriMatcher.addURI(AUTHORITY, Photos.TABLE, MATCH_PHOTO); @@ -285,29 +295,20 @@ public class PhotoProvider extends ContentProvider { sUriMatcher.addURI(AUTHORITY, Metadata.TABLE + "/#", MATCH_METADATA_ID); // match against image_cache/<ImageCache.PHOTO_ID> sUriMatcher.addURI(AUTHORITY, ImageCache.TABLE + "/#", MATCH_IMAGE); + // match against image_cache/album/<Albums._ID> + sUriMatcher.addURI(AUTHORITY, ImageCache.TABLE + "/" + Albums.TABLE + "/#", + MATCH_ALBUM_COVER); } @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { + public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs, + boolean callerIsSyncAdapter) { int match = matchUri(uri); - if (match == MATCH_IMAGE) { - throw new IllegalArgumentException("Cannot delete from image cache"); - } selection = addIdToSelection(match, selection); selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs); - List<Uri> changeUris = new ArrayList<Uri>(); int deleted = 0; - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - db.beginTransaction(); - try { - deleted = deleteCascade(db, match, selection, selectionArgs, changeUris, uri); - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - for (Uri changeUri : changeUris) { - notifyChanges(changeUri); - } + SQLiteDatabase db = getDatabaseHelper().getWritableDatabase(); + deleted = deleteCascade(db, match, selection, selectionArgs, uri); return deleted; } @@ -323,20 +324,19 @@ public class PhotoProvider extends ContentProvider { } @Override - public Uri insert(Uri uri, ContentValues values) { - // Cannot insert into this ContentProvider - return null; - } - - @Override - public boolean onCreate() { - mOpenHelper = createDatabaseHelper(); - return true; - } - - @Override - public void shutdown() { - getDatabaseHelper().close(); + public Uri insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { + int match = matchUri(uri); + validateMatchTable(match); + String table = getTableFromMatch(match, uri); + SQLiteDatabase db = getDatabaseHelper().getWritableDatabase(); + Uri insertedUri = null; + long id = db.insert(table, null, values); + if (id != -1) { + // uri already matches the table. + insertedUri = ContentUris.withAppendedId(uri, id); + postNotifyUri(insertedUri); + } + return insertedUri; } @Override @@ -352,31 +352,26 @@ public class PhotoProvider extends ContentProvider { selection = addIdToSelection(match, selection); selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs); String table = getTableFromMatch(match, uri); - SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + SQLiteDatabase db = getDatabaseHelper().getReadableDatabase(); return db.query(false, table, projection, selection, selectionArgs, null, null, sortOrder, null, cancellationSignal); } @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + public int updateInTransaction(Uri uri, ContentValues values, String selection, + String[] selectionArgs, boolean callerIsSyncAdapter) { int match = matchUri(uri); int rowsUpdated = 0; - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - db.beginTransaction(); - try { - if (match == MATCH_METADATA) { - rowsUpdated = modifyMetadata(db, values); - } else { - selection = addIdToSelection(match, selection); - selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs); - String table = getTableFromMatch(match, uri); - rowsUpdated = db.update(table, values, selection, selectionArgs); - } - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); + SQLiteDatabase db = getDatabaseHelper().getWritableDatabase(); + if (match == MATCH_METADATA) { + rowsUpdated = modifyMetadata(db, values); + } else { + selection = addIdToSelection(match, selection); + selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs); + String table = getTableFromMatch(match, uri); + rowsUpdated = db.update(table, values, selection, selectionArgs); } - notifyChanges(uri); + postNotifyUri(uri); return rowsUpdated; } @@ -445,31 +440,21 @@ public class PhotoProvider extends ContentProvider { return table; } - protected final SQLiteOpenHelper getDatabaseHelper() { - return mOpenHelper; - } - - protected SQLiteOpenHelper createDatabaseHelper() { - return new PhotoDatabase(getContext(), DB_NAME); + @Override + public SQLiteOpenHelper getDatabaseHelper(Context context) { + return new PhotoDatabase(context, DB_NAME); } private int modifyMetadata(SQLiteDatabase db, ContentValues values) { - String[] selectionArgs = { - values.getAsString(Metadata.PHOTO_ID), - values.getAsString(Metadata.KEY), - }; int rowCount; if (values.get(Metadata.VALUE) == null) { + String[] selectionArgs = { + values.getAsString(Metadata.PHOTO_ID), values.getAsString(Metadata.KEY), + }; rowCount = db.delete(Metadata.TABLE, WHERE_METADATA_ID, selectionArgs); } else { - rowCount = (int) DatabaseUtils.queryNumEntries(db, Metadata.TABLE, WHERE_METADATA_ID, - selectionArgs); - if (rowCount > 0) { - db.update(Metadata.TABLE, values, WHERE_METADATA_ID, selectionArgs); - } else { - db.insert(Metadata.TABLE, null, values); - rowCount = 1; - } + long rowId = db.replace(Metadata.TABLE, null, values); + rowCount = (rowId == -1) ? 0 : 1; } return rowCount; } @@ -479,14 +464,18 @@ public class PhotoProvider extends ContentProvider { if (match == UriMatcher.NO_MATCH) { throw unknownUri(uri); } + if (match == MATCH_IMAGE || match == MATCH_ALBUM_COVER) { + throw new IllegalArgumentException("Operation not allowed on image cache database"); + } return match; } - protected void notifyChanges(Uri uri) { + @Override + protected void notifyChange(ContentResolver resolver, Uri uri, boolean syncToNetwork) { if (mNotifier != null) { - mNotifier.notifyChange(uri); + mNotifier.notifyChange(uri, syncToNetwork); } else { - getContext().getContentResolver().notifyChange(uri, null, false); + resolver.notifyChange(uri, null, syncToNetwork); } } @@ -500,44 +489,55 @@ public class PhotoProvider extends ContentProvider { return matchColumn + IN + NESTED_SELECT_START + query + NESTED_SELECT_END; } - protected static int deleteCascade(SQLiteDatabase db, int match, String selection, - String[] selectionArgs, List<Uri> changeUris, Uri uri) { + protected int deleteCascade(SQLiteDatabase db, int match, String selection, + String[] selectionArgs, Uri uri) { switch (match) { case MATCH_PHOTO: case MATCH_PHOTO_ID: { - deleteCascadeMetadata(db, selection, selectionArgs, changeUris); + deleteCascadeMetadata(db, selection, selectionArgs); break; } case MATCH_ALBUM: case MATCH_ALBUM_ID: { - deleteCascadePhotos(db, selection, selectionArgs, changeUris); + deleteCascadePhotos(db, selection, selectionArgs); break; } } String table = getTableFromMatch(match, uri); int deleted = db.delete(table, selection, selectionArgs); if (deleted > 0) { - changeUris.add(uri); + postNotifyUri(uri); } return deleted; } - private static void deleteCascadePhotos(SQLiteDatabase db, String albumSelect, - String[] selectArgs, List<Uri> changeUris) { + private void deleteCascadePhotos(SQLiteDatabase db, String albumSelect, + String[] selectArgs) { String photoWhere = nestWhere(Photos.ALBUM_ID, Albums.TABLE, albumSelect); - deleteCascadeMetadata(db, photoWhere, selectArgs, changeUris); + deleteCascadeMetadata(db, photoWhere, selectArgs); int deleted = db.delete(Photos.TABLE, photoWhere, selectArgs); if (deleted > 0) { - changeUris.add(Photos.CONTENT_URI); + postNotifyUri(Photos.CONTENT_URI); } } - private static void deleteCascadeMetadata(SQLiteDatabase db, String photosSelect, - String[] selectArgs, List<Uri> changeUris) { + private void deleteCascadeMetadata(SQLiteDatabase db, String photosSelect, + String[] selectArgs) { String metadataWhere = nestWhere(Metadata.PHOTO_ID, Photos.TABLE, photosSelect); int deleted = db.delete(Metadata.TABLE, metadataWhere, selectArgs); if (deleted > 0) { - changeUris.add(Metadata.CONTENT_URI); + postNotifyUri(Metadata.CONTENT_URI); + } + } + + private static void validateMatchTable(int match) { + switch (match) { + case MATCH_PHOTO: + case MATCH_ALBUM: + case MATCH_METADATA: + break; + default: + throw new IllegalArgumentException("Operation not allowed on an existing row."); } } } diff --git a/src/com/android/photos/data/PhotoSetLoader.java b/src/com/android/photos/data/PhotoSetLoader.java index 8c511a525..21da90694 100644 --- a/src/com/android/photos/data/PhotoSetLoader.java +++ b/src/com/android/photos/data/PhotoSetLoader.java @@ -19,15 +19,20 @@ package com.android.photos.data; import android.content.Context; import android.content.CursorLoader; import android.database.ContentObserver; +import android.database.Cursor; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.provider.MediaStore; import android.provider.MediaStore.Files; import android.provider.MediaStore.Files.FileColumns; -public class PhotoSetLoader extends CursorLoader { +import com.android.photos.drawables.DataUriThumbnailDrawable; +import com.android.photos.drawables.DrawableFactory; + +public class PhotoSetLoader extends CursorLoader implements DrawableFactory<Cursor> { private static final Uri CONTENT_URI = Files.getContentUri("external"); - private static final String[] PROJECTION = new String[] { + public static final String[] PROJECTION = new String[] { FileColumns._ID, FileColumns.DATA, FileColumns.WIDTH, @@ -67,4 +72,17 @@ public class PhotoSetLoader extends CursorLoader { super.onReset(); getContext().getContentResolver().unregisterContentObserver(mGlobalObserver); } + + @Override + public Drawable drawableForItem(Cursor item, Drawable recycle) { + DataUriThumbnailDrawable drawable = null; + if (recycle == null || !(recycle instanceof DataUriThumbnailDrawable)) { + drawable = new DataUriThumbnailDrawable(); + } else { + drawable = (DataUriThumbnailDrawable) recycle; + } + drawable.setImage(item.getString(INDEX_DATA), + item.getInt(INDEX_WIDTH), item.getInt(INDEX_HEIGHT)); + return drawable; + } } diff --git a/src/com/android/photos/data/SQLiteContentProvider.java b/src/com/android/photos/data/SQLiteContentProvider.java new file mode 100644 index 000000000..ecd868b52 --- /dev/null +++ b/src/com/android/photos/data/SQLiteContentProvider.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.photos.data; + +import android.content.ContentProvider; +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.net.Uri; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +/** + * General purpose {@link ContentProvider} base class that uses SQLiteDatabase + * for storage. + */ +public abstract class SQLiteContentProvider extends ContentProvider { + + @SuppressWarnings("unused") + private static final String TAG = "SQLiteContentProvider"; + + private SQLiteOpenHelper mOpenHelper; + private Set<Uri> mChangedUris; + + private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>(); + private static final int SLEEP_AFTER_YIELD_DELAY = 4000; + + /** + * Maximum number of operations allowed in a batch between yield points. + */ + private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500; + + @Override + public boolean onCreate() { + Context context = getContext(); + mOpenHelper = getDatabaseHelper(context); + mChangedUris = new HashSet<Uri>(); + return true; + } + + @Override + public void shutdown() { + getDatabaseHelper().close(); + } + + /** + * Returns a {@link SQLiteOpenHelper} that can open the database. + */ + public abstract SQLiteOpenHelper getDatabaseHelper(Context context); + + /** + * The equivalent of the {@link #insert} method, but invoked within a + * transaction. + */ + public abstract Uri insertInTransaction(Uri uri, ContentValues values, + boolean callerIsSyncAdapter); + + /** + * The equivalent of the {@link #update} method, but invoked within a + * transaction. + */ + public abstract int updateInTransaction(Uri uri, ContentValues values, String selection, + String[] selectionArgs, boolean callerIsSyncAdapter); + + /** + * The equivalent of the {@link #delete} method, but invoked within a + * transaction. + */ + public abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs, + boolean callerIsSyncAdapter); + + /** + * Call this to add a URI to the list of URIs to be notified when the + * transaction is committed. + */ + protected void postNotifyUri(Uri uri) { + synchronized (mChangedUris) { + mChangedUris.add(uri); + } + } + + public boolean isCallerSyncAdapter(Uri uri) { + return false; + } + + public SQLiteOpenHelper getDatabaseHelper() { + return mOpenHelper; + } + + private boolean applyingBatch() { + return mApplyingBatch.get() != null && mApplyingBatch.get(); + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + Uri result = null; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + boolean applyingBatch = applyingBatch(); + if (!applyingBatch) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + result = insertInTransaction(uri, values, callerIsSyncAdapter); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + onEndTransaction(callerIsSyncAdapter); + } else { + result = insertInTransaction(uri, values, callerIsSyncAdapter); + } + return result; + } + + @Override + public int bulkInsert(Uri uri, ContentValues[] values) { + int numValues = values.length; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + for (int i = 0; i < numValues; i++) { + @SuppressWarnings("unused") + Uri result = insertInTransaction(uri, values[i], callerIsSyncAdapter); + db.yieldIfContendedSafely(); + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + onEndTransaction(callerIsSyncAdapter); + return numValues; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + int count = 0; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + boolean applyingBatch = applyingBatch(); + if (!applyingBatch) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + count = updateInTransaction(uri, values, selection, selectionArgs, + callerIsSyncAdapter); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + onEndTransaction(callerIsSyncAdapter); + } else { + count = updateInTransaction(uri, values, selection, selectionArgs, callerIsSyncAdapter); + } + + return count; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + int count = 0; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + boolean applyingBatch = applyingBatch(); + if (!applyingBatch) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + count = deleteInTransaction(uri, selection, selectionArgs, callerIsSyncAdapter); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + onEndTransaction(callerIsSyncAdapter); + } else { + count = deleteInTransaction(uri, selection, selectionArgs, callerIsSyncAdapter); + } + return count; + } + + @Override + public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) + throws OperationApplicationException { + int ypCount = 0; + int opCount = 0; + boolean callerIsSyncAdapter = false; + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + mApplyingBatch.set(true); + final int numOperations = operations.size(); + final ContentProviderResult[] results = new ContentProviderResult[numOperations]; + for (int i = 0; i < numOperations; i++) { + if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) { + throw new OperationApplicationException( + "Too many content provider operations between yield points. " + + "The maximum number of operations per yield point is " + + MAX_OPERATIONS_PER_YIELD_POINT, ypCount); + } + final ContentProviderOperation operation = operations.get(i); + if (!callerIsSyncAdapter && isCallerSyncAdapter(operation.getUri())) { + callerIsSyncAdapter = true; + } + if (i > 0 && operation.isYieldAllowed()) { + opCount = 0; + if (db.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY)) { + ypCount++; + } + } + results[i] = operation.apply(this, results, i); + } + db.setTransactionSuccessful(); + return results; + } finally { + mApplyingBatch.set(false); + db.endTransaction(); + onEndTransaction(callerIsSyncAdapter); + } + } + + protected void onEndTransaction(boolean callerIsSyncAdapter) { + Set<Uri> changed; + synchronized (mChangedUris) { + changed = new HashSet<Uri>(mChangedUris); + mChangedUris.clear(); + } + ContentResolver resolver = getContext().getContentResolver(); + for (Uri uri : changed) { + boolean syncToNetwork = !callerIsSyncAdapter && syncToNetwork(uri); + notifyChange(resolver, uri, syncToNetwork); + } + } + + protected void notifyChange(ContentResolver resolver, Uri uri, boolean syncToNetwork) { + resolver.notifyChange(uri, null, syncToNetwork); + } + + protected boolean syncToNetwork(Uri uri) { + return false; + } +}
\ No newline at end of file diff --git a/src/com/android/photos/drawables/DrawableFactory.java b/src/com/android/photos/drawables/DrawableFactory.java new file mode 100644 index 000000000..ad046c820 --- /dev/null +++ b/src/com/android/photos/drawables/DrawableFactory.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.photos.drawables; + +import android.graphics.drawable.Drawable; + + +public interface DrawableFactory<T> { + Drawable drawableForItem(T item, Drawable recycle); +} diff --git a/src/com/android/photos/shims/BitmapJobDrawable.java b/src/com/android/photos/shims/BitmapJobDrawable.java new file mode 100644 index 000000000..299becb07 --- /dev/null +++ b/src/com/android/photos/shims/BitmapJobDrawable.java @@ -0,0 +1,158 @@ +package com.android.photos.shims; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.ui.BitmapLoader; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.ThreadPool; +import com.android.photos.data.GalleryBitmapPool; +import com.android.photos.drawables.AutoThumbnailDrawable; + + +public class BitmapJobDrawable extends Drawable implements Runnable { + + private ThumbnailLoader mLoader; + private MediaItem mItem; + private Bitmap mBitmap; + private Paint mPaint = new Paint(); + private Matrix mDrawMatrix = new Matrix(); + + public BitmapJobDrawable() { + } + + public void setMediaItem(MediaItem item) { + if (mLoader != null) { + mLoader.cancelLoad(); + } + mItem = item; + if (mBitmap != null) { + GalleryBitmapPool.getInstance().put(mBitmap); + mBitmap = null; + } + // TODO: Figure out why ThumbnailLoader doesn't like to be re-used + mLoader = new ThumbnailLoader(this); + mLoader.startLoad(); + invalidateSelf(); + } + + @Override + public void run() { + Bitmap bitmap = mLoader.getBitmap(); + if (bitmap != null) { + mBitmap = bitmap; + updateDrawMatrix(); + } + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + updateDrawMatrix(); + } + + @Override + public void draw(Canvas canvas) { + Rect bounds = getBounds(); + if (mBitmap != null) { + canvas.save(); + canvas.clipRect(bounds); + canvas.concat(mDrawMatrix); + canvas.drawBitmap(mBitmap, 0, 0, mPaint); + canvas.restore(); + } else { + mPaint.setColor(0xFFCCCCCC); + canvas.drawRect(bounds, mPaint); + } + } + + private void updateDrawMatrix() { + Rect bounds = getBounds(); + if (mBitmap == null || bounds.isEmpty()) { + mDrawMatrix.reset(); + return; + } + + float scale; + float dx = 0, dy = 0; + + int dwidth = mBitmap.getWidth(); + int dheight = mBitmap.getHeight(); + int vwidth = bounds.width(); + int vheight = bounds.height(); + + // Calculates a matrix similar to ScaleType.CENTER_CROP + if (dwidth * vheight > vwidth * dheight) { + scale = (float) vheight / (float) dheight; + dx = (vwidth - dwidth * scale) * 0.5f; + } else { + scale = (float) vwidth / (float) dwidth; + dy = (vheight - dheight * scale) * 0.5f; + } + + mDrawMatrix.setScale(scale, scale); + mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f)); + invalidateSelf(); + } + + @Override + public int getIntrinsicWidth() { + return MediaItem.getTargetSize(MediaItem.TYPE_MICROTHUMBNAIL); + } + + @Override + public int getIntrinsicHeight() { + return MediaItem.getTargetSize(MediaItem.TYPE_MICROTHUMBNAIL); + } + + @Override + public int getOpacity() { + Bitmap bm = mBitmap; + return (bm == null || bm.hasAlpha() || mPaint.getAlpha() < 255) ? + PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE; + } + + @Override + public void setAlpha(int alpha) { + int oldAlpha = mPaint.getAlpha(); + if (alpha != oldAlpha) { + mPaint.setAlpha(alpha); + invalidateSelf(); + } + } + + @Override + public void setColorFilter(ColorFilter cf) { + mPaint.setColorFilter(cf); + invalidateSelf(); + } + + private static class ThumbnailLoader extends BitmapLoader { + private static final ThreadPool sThreadPool = new ThreadPool(0, 2); + private BitmapJobDrawable mParent; + + public ThumbnailLoader(BitmapJobDrawable parent) { + mParent = parent; + } + + @Override + protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) { + return sThreadPool.submit( + mParent.mItem.requestImage(MediaItem.TYPE_MICROTHUMBNAIL), this); + } + + @Override + protected void onLoadComplete(Bitmap bitmap) { + mParent.scheduleSelf(mParent, 0); + } + } + +} diff --git a/src/com/android/photos/shims/MediaItemsLoader.java b/src/com/android/photos/shims/MediaItemsLoader.java new file mode 100644 index 000000000..886b3c3a1 --- /dev/null +++ b/src/com/android/photos/shims/MediaItemsLoader.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.photos.shims; + +import android.content.AsyncTaskLoader; +import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.graphics.drawable.Drawable; +import android.provider.MediaStore.Files.FileColumns; + +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.MediaSet.ItemConsumer; +import com.android.gallery3d.data.MediaSet.SyncListener; +import com.android.gallery3d.util.Future; +import com.android.photos.data.PhotoSetLoader; +import com.android.photos.drawables.DrawableFactory; + +import java.util.ArrayList; + +/** + * Returns all MediaItems in a MediaSet, wrapping them in a cursor to appear + * like a PhotoSetLoader + */ +public class MediaItemsLoader extends AsyncTaskLoader<Cursor> implements DrawableFactory<Cursor> { + + private static final SyncListener sNullListener = new SyncListener() { + @Override + public void onSyncDone(MediaSet mediaSet, int resultCode) { + } + }; + + private MediaSet mMediaSet; + private Future<Integer> mSyncTask = null; + private ContentListener mObserver = new ContentListener() { + @Override + public void onContentDirty() { + onContentChanged(); + } + }; + private ArrayList<MediaItem> mMediaItems = new ArrayList<MediaItem>(); + + public MediaItemsLoader(Context context) { + super(context); + DataManager dm = DataManager.from(context); + String path = dm.getTopSetPath(DataManager.INCLUDE_ALL); + mMediaSet = dm.getMediaSet(path); + } + + public MediaItemsLoader(Context context, String parentPath) { + super(context); + mMediaSet = DataManager.from(getContext()).getMediaSet(parentPath); + } + + @Override + protected void onStartLoading() { + super.onStartLoading(); + mMediaSet.addContentListener(mObserver); + mSyncTask = mMediaSet.requestSync(sNullListener); + forceLoad(); + } + + @Override + protected boolean onCancelLoad() { + if (mSyncTask != null) { + mSyncTask.cancel(); + mSyncTask = null; + } + return super.onCancelLoad(); + } + + @Override + protected void onStopLoading() { + super.onStopLoading(); + cancelLoad(); + mMediaSet.removeContentListener(mObserver); + } + + @Override + protected void onReset() { + super.onReset(); + onStopLoading(); + } + + @Override + public Cursor loadInBackground() { + mMediaSet.loadIfDirty(); + final MatrixCursor cursor = new MatrixCursor(PhotoSetLoader.PROJECTION); + final Object[] row = new Object[PhotoSetLoader.PROJECTION.length]; + mMediaSet.enumerateTotalMediaItems(new ItemConsumer() { + @Override + public void consume(int index, MediaItem item) { + row[PhotoSetLoader.INDEX_ID] = index; + row[PhotoSetLoader.INDEX_DATA] = item.getContentUri().toString(); + row[PhotoSetLoader.INDEX_DATE_ADDED] = item.getDateInMs(); + row[PhotoSetLoader.INDEX_HEIGHT] = item.getHeight(); + row[PhotoSetLoader.INDEX_WIDTH] = item.getWidth(); + row[PhotoSetLoader.INDEX_WIDTH] = item.getWidth(); + int rawMediaType = item.getMediaType(); + int mappedMediaType = FileColumns.MEDIA_TYPE_NONE; + if (rawMediaType == MediaItem.MEDIA_TYPE_IMAGE) { + mappedMediaType = FileColumns.MEDIA_TYPE_IMAGE; + } else if (rawMediaType == MediaItem.MEDIA_TYPE_VIDEO) { + mappedMediaType = FileColumns.MEDIA_TYPE_VIDEO; + } + row[PhotoSetLoader.INDEX_MEDIA_TYPE] = mappedMediaType; + cursor.addRow(row); + mMediaItems.add(item); + } + }); + return cursor; + } + + @Override + public Drawable drawableForItem(Cursor item, Drawable recycle) { + BitmapJobDrawable drawable = null; + if (recycle == null || !(recycle instanceof BitmapJobDrawable)) { + drawable = new BitmapJobDrawable(); + } else { + drawable = (BitmapJobDrawable) recycle; + } + int index = item.getInt(PhotoSetLoader.INDEX_ID); + drawable.setMediaItem(mMediaItems.get(index)); + return drawable; + } + + public static int getThumbnailSize() { + return MediaItem.getTargetSize(MediaItem.TYPE_MICROTHUMBNAIL); + } + +} diff --git a/src/com/android/photos/shims/MediaSetLoader.java b/src/com/android/photos/shims/MediaSetLoader.java new file mode 100644 index 000000000..7a6fcb865 --- /dev/null +++ b/src/com/android/photos/shims/MediaSetLoader.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.photos.shims; + +import android.content.AsyncTaskLoader; +import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.graphics.drawable.Drawable; +import android.provider.MediaStore.Files.FileColumns; + +import com.android.gallery3d.data.ContentListener; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaDetails; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.MediaSet.ItemConsumer; +import com.android.gallery3d.data.MediaSet.SyncListener; +import com.android.gallery3d.util.Future; +import com.android.photos.data.AlbumSetLoader; +import com.android.photos.data.PhotoSetLoader; +import com.android.photos.drawables.DrawableFactory; + +import java.text.DateFormat; +import java.text.ParseException; +import java.util.ArrayList; + +/** + * Returns all MediaSets in a MediaSet, wrapping them in a cursor to appear + * like a AlbumSetLoader. + */ +public class MediaSetLoader extends AsyncTaskLoader<Cursor> implements DrawableFactory<Cursor>{ + + private static final SyncListener sNullListener = new SyncListener() { + @Override + public void onSyncDone(MediaSet mediaSet, int resultCode) { + } + }; + + private MediaSet mMediaSet; + private Future<Integer> mSyncTask = null; + private ContentListener mObserver = new ContentListener() { + @Override + public void onContentDirty() { + onContentChanged(); + } + }; + + private ArrayList<MediaItem> mCoverItems = new ArrayList<MediaItem>(); + + public MediaSetLoader(Context context) { + super(context); + DataManager dm = DataManager.from(context); + String path = dm.getTopSetPath(DataManager.INCLUDE_ALL); + mMediaSet = dm.getMediaSet(path); + } + + public MediaSetLoader(Context context, String path) { + super(context); + mMediaSet = DataManager.from(getContext()).getMediaSet(path); + } + + @Override + protected void onStartLoading() { + super.onStartLoading(); + mMediaSet.addContentListener(mObserver); + mSyncTask = mMediaSet.requestSync(sNullListener); + forceLoad(); + } + + @Override + protected boolean onCancelLoad() { + if (mSyncTask != null) { + mSyncTask.cancel(); + mSyncTask = null; + } + return super.onCancelLoad(); + } + + @Override + protected void onStopLoading() { + super.onStopLoading(); + cancelLoad(); + mMediaSet.removeContentListener(mObserver); + } + + @Override + protected void onReset() { + super.onReset(); + onStopLoading(); + } + + @Override + public Cursor loadInBackground() { + mMediaSet.loadIfDirty(); + final MatrixCursor cursor = new MatrixCursor(AlbumSetLoader.PROJECTION); + final Object[] row = new Object[AlbumSetLoader.PROJECTION.length]; + int count = mMediaSet.getSubMediaSetCount(); + for (int i = 0; i < count; i++) { + MediaSet m = mMediaSet.getSubMediaSet(i); + m.loadIfDirty(); + row[AlbumSetLoader.INDEX_ID] = i; + row[AlbumSetLoader.INDEX_TITLE] = m.getName(); + row[AlbumSetLoader.INDEX_COUNT] = m.getMediaItemCount(); + MediaItem coverItem = m.getCoverMediaItem(); + row[AlbumSetLoader.INDEX_TIMESTAMP] = coverItem.getDateInMs(); + mCoverItems.add(coverItem); + cursor.addRow(row); + } + return cursor; + } + + @Override + public Drawable drawableForItem(Cursor item, Drawable recycle) { + BitmapJobDrawable drawable = null; + if (recycle == null || !(recycle instanceof BitmapJobDrawable)) { + drawable = new BitmapJobDrawable(); + } else { + drawable = (BitmapJobDrawable) recycle; + } + int index = item.getInt(AlbumSetLoader.INDEX_ID); + drawable.setMediaItem(mCoverItems.get(index)); + return drawable; + } + + public static int getThumbnailSize() { + return MediaItem.getTargetSize(MediaItem.TYPE_MICROTHUMBNAIL); + } +} diff --git a/tests/src/com/android/photos/data/PhotoDatabaseUtils.java b/tests/src/com/android/photos/data/PhotoDatabaseUtils.java index 1840eb1be..97db8bf7d 100644 --- a/tests/src/com/android/photos/data/PhotoDatabaseUtils.java +++ b/tests/src/com/android/photos/data/PhotoDatabaseUtils.java @@ -31,7 +31,6 @@ public class PhotoDatabaseUtils { Albums._ID, Albums.ACCOUNT_ID, Albums.PARENT_ID, - Albums.NAME, Albums.VISIBILITY, Albums.LOCATION_STRING, Albums.TITLE, @@ -105,11 +104,11 @@ public class PhotoDatabaseUtils { return db.insert(Photos.TABLE, null, values) != -1; } - public static boolean insertAlbum(SQLiteDatabase db, Long parentId, String name, + public static boolean insertAlbum(SQLiteDatabase db, Long parentId, String title, Integer privacy, Long accountId) { ContentValues values = new ContentValues(); values.put(Albums.PARENT_ID, parentId); - values.put(Albums.NAME, name); + values.put(Albums.TITLE, title); values.put(Albums.VISIBILITY, privacy); values.put(Albums.ACCOUNT_ID, accountId); return db.insert(Albums.TABLE, null, values) != -1; diff --git a/tests/src/com/android/photos/data/PhotoProviderTest.java b/tests/src/com/android/photos/data/PhotoProviderTest.java index 2e644a3d6..39abff441 100644 --- a/tests/src/com/android/photos/data/PhotoProviderTest.java +++ b/tests/src/com/android/photos/data/PhotoProviderTest.java @@ -15,13 +15,16 @@ */ package com.android.photos.data; +import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; +import android.content.OperationApplicationException; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; +import android.os.RemoteException; import android.provider.BaseColumns; import android.test.ProviderTestCase2; @@ -29,12 +32,14 @@ import com.android.photos.data.PhotoProvider.Albums; import com.android.photos.data.PhotoProvider.Metadata; import com.android.photos.data.PhotoProvider.Photos; +import java.util.ArrayList; + public class PhotoProviderTest extends ProviderTestCase2<PhotoProvider> { @SuppressWarnings("unused") private static final String TAG = PhotoProviderTest.class.getSimpleName(); private static final String MIME_TYPE = "test/test"; - private static final String ALBUM_NAME = "My Album"; + private static final String ALBUM_TITLE = "My Album"; private static final long ALBUM_PARENT_ID = 100; private static final String META_KEY = "mykey"; private static final String META_VALUE = "myvalue"; @@ -69,7 +74,7 @@ public class PhotoProviderTest extends ProviderTestCase2<PhotoProvider> { SQLiteDatabase db = mDBHelper.getWritableDatabase(); db.beginTransaction(); try { - PhotoDatabaseUtils.insertAlbum(db, ALBUM_PARENT_ID, ALBUM_NAME, + PhotoDatabaseUtils.insertAlbum(db, ALBUM_PARENT_ID, ALBUM_TITLE, Albums.VISIBILITY_PRIVATE, 100L); mAlbumId = PhotoDatabaseUtils.queryAlbumIdFromParentId(db, ALBUM_PARENT_ID); PhotoDatabaseUtils.insertPhoto(db, 100, 100, System.currentTimeMillis(), mAlbumId, @@ -188,19 +193,32 @@ public class PhotoProviderTest extends ProviderTestCase2<PhotoProvider> { public void testInsert() { ContentValues values = new ContentValues(); - values.put(Albums.NAME, "don't add me"); + values.put(Albums.TITLE, "add me"); values.put(Albums.VISIBILITY, Albums.VISIBILITY_PRIVATE); - assertNull(mResolver.insert(Albums.CONTENT_URI, values)); + values.put(Albums.ACCOUNT_ID, 100L); + values.put(Albums.DATE_MODIFIED, 100L); + values.put(Albums.DATE_PUBLISHED, 100L); + values.put(Albums.LOCATION_STRING, "Home"); + values.put(Albums.TITLE, "hello world"); + values.putNull(Albums.PARENT_ID); + values.put(Albums.SUMMARY, "Nothing much to say about this"); + Uri insertedUri = mResolver.insert(Albums.CONTENT_URI, values); + assertNotNull(insertedUri); + Cursor cursor = mResolver.query(insertedUri, PhotoDatabaseUtils.PROJECTION_ALBUMS, null, + null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.close(); } public void testUpdate() { ContentValues values = new ContentValues(); // Normal update -- use an album. - values.put(Albums.NAME, "foo"); + values.put(Albums.TITLE, "foo"); Uri albumUri = ContentUris.withAppendedId(Albums.CONTENT_URI, mAlbumId); assertEquals(1, mResolver.update(albumUri, values, null, null)); String[] projection = { - Albums.NAME, + Albums.TITLE, }; Cursor cursor = mResolver.query(albumUri, projection, null, null, null); assertEquals(1, cursor.getCount()); @@ -210,7 +228,7 @@ public class PhotoProviderTest extends ProviderTestCase2<PhotoProvider> { // Update a row that doesn't exist. Uri noAlbumUri = ContentUris.withAppendedId(Albums.CONTENT_URI, mAlbumId + 1); - values.put(Albums.NAME, "bar"); + values.put(Albums.TITLE, "bar"); assertEquals(0, mResolver.update(noAlbumUri, values, null, null)); // Update a metadata value that exists. @@ -304,4 +322,38 @@ public class PhotoProviderTest extends ProviderTestCase2<PhotoProvider> { mResolver.update(Metadata.CONTENT_URI, values, null, null); assertTrue(mNotifications.isNotified(Metadata.CONTENT_URI)); } + + public void testBatchTransaction() throws RemoteException, OperationApplicationException { + ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(); + ContentProviderOperation.Builder insert = ContentProviderOperation + .newInsert(Photos.CONTENT_URI); + insert.withValue(Photos.WIDTH, 200L); + insert.withValue(Photos.HEIGHT, 100L); + insert.withValue(Photos.DATE_TAKEN, System.currentTimeMillis()); + insert.withValue(Photos.ALBUM_ID, 1000L); + insert.withValue(Photos.MIME_TYPE, "image/jpg"); + insert.withValue(Photos.ACCOUNT_ID, 1L); + operations.add(insert.build()); + ContentProviderOperation.Builder update = ContentProviderOperation.newUpdate(Photos.CONTENT_URI); + update.withValue(Photos.DATE_MODIFIED, System.currentTimeMillis()); + String[] whereArgs = { + "100", + }; + String where = Photos.WIDTH + " = ?"; + update.withSelection(where, whereArgs); + operations.add(update.build()); + ContentProviderOperation.Builder delete = ContentProviderOperation + .newDelete(Photos.CONTENT_URI); + delete.withSelection(where, whereArgs); + operations.add(delete.build()); + mResolver.applyBatch(PhotoProvider.AUTHORITY, operations); + assertEquals(3, mNotifications.notificationCount()); + SQLiteDatabase db = mDBHelper.getReadableDatabase(); + long id = PhotoDatabaseUtils.queryPhotoIdFromAlbumId(db, 1000L); + Uri uri = ContentUris.withAppendedId(Photos.CONTENT_URI, id); + assertTrue(mNotifications.isNotified(uri)); + assertTrue(mNotifications.isNotified(Metadata.CONTENT_URI)); + assertTrue(mNotifications.isNotified(Photos.CONTENT_URI)); + } + } |