OSDN Git Service

reconcile main tree with open-source eclair
authorThe Android Open Source Project <initial-contribution@android.com>
Fri, 5 Feb 2010 16:09:39 +0000 (08:09 -0800)
committerThe Android Open Source Project <initial-contribution@android.com>
Fri, 5 Feb 2010 16:09:39 +0000 (08:09 -0800)
src/com/cooliris/cache/BootReceiver.java
src/com/cooliris/cache/CacheService.java
src/com/cooliris/media/Gallery.java
src/com/cooliris/media/GridInputProcessor.java
src/com/cooliris/media/GridLayer.java
src/com/cooliris/media/HudLayer.java
src/com/cooliris/media/ScaleGestureDetector.java [new file with mode: 0644]
src/com/cooliris/picasa/PicasaContentProvider.java
src/com/cooliris/picasa/PicasaSyncAdapter.java

index a976326..2ca5455 100644 (file)
@@ -18,6 +18,7 @@ import android.util.Log;
 public class BootReceiver extends BroadcastReceiver {
     private static final String TAG = "BootReceiver";
     private final Handler mHandler = new Handler();
+    private boolean mListenersInitialized = false;
 
     @Override
     public void onReceive(final Context context, Intent intent) {
@@ -27,36 +28,37 @@ public class BootReceiver extends BroadcastReceiver {
             CacheService.markDirty(context);
             CacheService.startCache(context, true);
         } else if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
-            // Do nothing, wait for the mediascanner to be done after mounting.
-            ;
+            if (!mListenersInitialized) {
+                // We add special listeners for the MediaProvider
+                mListenersInitialized = true;
+                final Handler handler = mHandler;
+                final ContentObserver localObserver = new ContentObserver(handler) {
+                    public void onChange(boolean selfChange) {
+                        if (!LocalDataSource.sObserverActive) {
+                            CacheService.senseDirty(context, null);
+                        }
+                    }
+                };
+                // Start listening perpetually.
+                Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI;
+                Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI;
+                ContentResolver cr = context.getContentResolver();
+                cr.registerContentObserver(uriImages, false, localObserver);
+                cr.registerContentObserver(uriVideos, false, localObserver);
+            }
         } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)) {
             final Uri fileUri = intent.getData();
             final long bucketId = SingleDataSource.parseBucketIdFromFileUri(fileUri.toString());
             if (!CacheService.isPresentInCache(bucketId)) {
                 CacheService.markDirty(context);
             }
-        } else if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
-            // We add special listeners for the MediaProvider
-            final Handler handler = mHandler;
-            final ContentObserver localObserver = new ContentObserver(handler) {
-                public void onChange(boolean selfChange) {
-                    if (!LocalDataSource.sObserverActive) {
-                        CacheService.senseDirty(context, null);
-                    }
-                }
-            };
-            // Start listening perpetually.
-            Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI;
-            Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI;
-            ContentResolver cr = context.getContentResolver();
-            cr.registerContentObserver(uriImages, false, localObserver);
-            cr.registerContentObserver(uriVideos, false, localObserver);
         } else if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
             LocalDataSource.sThumbnailCache.close();
             LocalDataSource.sThumbnailCacheVideo.close();
             PicasaDataSource.sThumbnailCache.close();
             CacheService.sAlbumCache.close();
             CacheService.sMetaAlbumCache.close();
+            CacheService.sSkipThumbnailIds.flush();
         }
     }
 }
index 602e207..6b8f9f0 100644 (file)
@@ -57,6 +57,7 @@ public final class CacheService extends IntentService {
     public static final String ACTION_CACHE = "com.cooliris.cache.action.CACHE";
     public static final DiskCache sAlbumCache = new DiskCache("local-album-cache");
     public static final DiskCache sMetaAlbumCache = new DiskCache("local-meta-cache");
+    public static final DiskCache sSkipThumbnailIds = new DiskCache("local-skip-cache");
 
     private static final String TAG = "CacheService";
     private static ImageList sList = null;
@@ -603,14 +604,36 @@ public final class CacheService extends IntentService {
             final long id = ids[i];
             final long timeModifiedInSec = timestamp[i];
             final long thumbnailId = thumbnailIds[i];
-            if (!thumbnailCache.isDataAvailable(thumbnailId, timeModifiedInSec * 1000)) {
-                buildThumbnailForId(context, thumbnailCache, thumbnailId, id, false, DEFAULT_THUMBNAIL_WIDTH,
-                        DEFAULT_THUMBNAIL_HEIGHT, timeModifiedInSec * 1000);
+            if (!isInThumbnailerSkipList(thumbnailId)) {
+                if (!thumbnailCache.isDataAvailable(thumbnailId, timeModifiedInSec * 1000)) {
+                    byte[] retVal = buildThumbnailForId(context, thumbnailCache, thumbnailId, id, false, DEFAULT_THUMBNAIL_WIDTH,
+                            DEFAULT_THUMBNAIL_HEIGHT, timeModifiedInSec * 1000);
+                    if (retVal == null || retVal.length == 0) {
+                        // There was an error in building the thumbnail.
+                        // We record this thumbnail id
+                        addToThumbnailerSkipList(thumbnailId);
+                    }
+                }
             }
         }
         Log.i(TAG, "DiskCache ready for all thumbnails.");
     }
 
+    private static void addToThumbnailerSkipList(long thumbnailId) {
+        sSkipThumbnailIds.put(thumbnailId, sDummyData, 0);
+        sSkipThumbnailIds.flush();
+    }
+
+    private static boolean isInThumbnailerSkipList(long thumbnailId) {
+        if (sSkipThumbnailIds.isDataAvailable(thumbnailId, 0)) {
+            byte[] data = sSkipThumbnailIds.get(thumbnailId, 0);
+            if (data.length > 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private static final byte[] buildThumbnailForId(final Context context, final DiskCache thumbnailCache, final long thumbId,
             final long origId, final boolean isVideo, final int thumbnailWidth, final int thumbnailHeight, final long timestamp) {
         if (origId == Shared.INVALID) {
@@ -650,7 +673,8 @@ public final class CacheService extends IntentService {
             if (bitmap == null) {
                 return null;
             }
-            final byte[] retVal = writeBitmapToCache(thumbnailCache, thumbId, origId, bitmap, thumbnailWidth, thumbnailHeight, timestamp);
+            final byte[] retVal = writeBitmapToCache(thumbnailCache, thumbId, origId, bitmap, thumbnailWidth, thumbnailHeight,
+                    timestamp);
             return retVal;
         } catch (InterruptedException e) {
             return null;
index 62d47e2..0ea80dd 100644 (file)
@@ -42,11 +42,12 @@ public final class Gallery extends Activity {
     private MediaScannerConnection mConnection;
     private WakeLock mWakeLock;
     private HashMap<String, Boolean> mAccountsEnabled = new HashMap<String, Boolean>();
+    private boolean mDockSlideshow = false;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        final boolean imageManagerHasStorage = ImageManager.quickHasStorage();
+        final boolean imageManagerHasStorage = ImageManager.hasStorage();
         boolean slideshowIntent = false;
         if (isViewIntent()) {
             Bundle extras = getIntent().getExtras();
@@ -60,17 +61,13 @@ public final class Gallery extends Activity {
                 Toast.makeText(this, getResources().getString(R.string.no_sd_card), Toast.LENGTH_LONG).show();
                 finish();
             } else {
-                PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
-                mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "GridView.Slideshow.All");
-                mWakeLock.acquire();
                 Slideshow slideshow = new Slideshow(this);
                 slideshow.setDataSource(new RandomDataSource());
                 setContentView(slideshow);
+                mDockSlideshow = true;
             }
             return;
         }
-        CacheService.computeDirtySets(this);
-        final boolean isCacheReady = CacheService.isCacheReady(false);
         if (PIXEL_DENSITY == 0.0f) {
             DisplayMetrics metrics = new DisplayMetrics();
             getWindowManager().getDefaultDisplay().getMetrics(metrics);
@@ -82,66 +79,94 @@ public final class Gallery extends Activity {
                 mRenderView);
         mRenderView.setRootLayer(mGridLayer);
         setContentView(mRenderView);
+        
+        Thread t = new Thread() {
+            public void run() {
+                int numRetries = 25;
+                if (!imageManagerHasStorage) {
+                    showToast(getResources().getString(R.string.no_sd_card), Toast.LENGTH_LONG);
+                    do {
+                        --numRetries;
+                        try {
+                        Thread.sleep(200);
+                        } catch (InterruptedException e) {
+                            ;
+                        }
+                    } while (numRetries > 0 && !ImageManager.hasStorage());
+                }
+                final boolean imageManagerHasStorageAfterDelay = ImageManager.hasStorage();
+                CacheService.computeDirtySets(Gallery.this);
+                CacheService.startCache(Gallery.this, false);
+                final boolean isCacheReady = CacheService.isCacheReady(false);
 
-        // Creating the DataSource objects.
-        final PicasaDataSource picasaDataSource = new PicasaDataSource(this);
-        final LocalDataSource localDataSource = new LocalDataSource(this);
-        final ConcatenatedDataSource combinedDataSource = new ConcatenatedDataSource(localDataSource, picasaDataSource);
+                // Creating the DataSource objects.
+                final PicasaDataSource picasaDataSource = new PicasaDataSource(Gallery.this);
+                final LocalDataSource localDataSource = new LocalDataSource(Gallery.this);
+                final ConcatenatedDataSource combinedDataSource = new ConcatenatedDataSource(localDataSource, picasaDataSource);
 
-        // Depending upon the intent, we assign the right dataSource.
-        if (!isPickIntent() && !isViewIntent()) {
-            if (imageManagerHasStorage) {
-                mGridLayer.setDataSource(combinedDataSource);
-            } else {
-                mGridLayer.setDataSource(picasaDataSource);
-            }
-            if (!imageManagerHasStorage) {
-                Toast.makeText(this, getResources().getString(R.string.no_sd_card), Toast.LENGTH_LONG).show();
-            } else if (!isCacheReady) {
-                Toast.makeText(this, getResources().getString(R.string.loading_new), Toast.LENGTH_LONG).show();
-            }
-        } else if (!isViewIntent()) {
-            final Intent intent = getIntent();
-            if (intent != null) {
-                final String type = intent.resolveType(this);
-                boolean includeImages = isImageType(type);
-                boolean includeVideos = isVideoType(type);
-                ((LocalDataSource) localDataSource).setMimeFilter(!includeImages, !includeVideos);
-                if (includeImages) {
-                    if (imageManagerHasStorage) {
+                // Depending upon the intent, we assign the right dataSource.
+                if (!isPickIntent() && !isViewIntent()) {
+                    if (imageManagerHasStorageAfterDelay) {
                         mGridLayer.setDataSource(combinedDataSource);
                     } else {
                         mGridLayer.setDataSource(picasaDataSource);
                     }
+                    if (!isCacheReady && imageManagerHasStorageAfterDelay) {
+                        showToast(getResources().getString(R.string.loading_new), Toast.LENGTH_LONG);
+                    }
+                } else if (!isViewIntent()) {
+                    final Intent intent = getIntent();
+                    if (intent != null) {
+                        final String type = intent.resolveType(Gallery.this);
+                        boolean includeImages = isImageType(type);
+                        boolean includeVideos = isVideoType(type);
+                        ((LocalDataSource) localDataSource).setMimeFilter(!includeImages, !includeVideos);
+                        if (includeImages) {
+                            if (imageManagerHasStorageAfterDelay) {
+                                mGridLayer.setDataSource(combinedDataSource);
+                            } else {
+                                mGridLayer.setDataSource(picasaDataSource);
+                            }
+                        } else {
+                            mGridLayer.setDataSource(localDataSource);
+                        }
+                        mGridLayer.setPickIntent(true);
+                        if (!imageManagerHasStorageAfterDelay) {
+                            showToast(getResources().getString(R.string.no_sd_card), Toast.LENGTH_LONG);
+                        } else {
+                            showToast(getResources().getString(R.string.pick_prompt), Toast.LENGTH_LONG);
+                        }
+                    }
                 } else {
-                    mGridLayer.setDataSource(localDataSource);
-                }
-                mGridLayer.setPickIntent(true);
-                if (!imageManagerHasStorage) {
-                    Toast.makeText(this, getResources().getString(R.string.no_sd_card), Toast.LENGTH_LONG).show();
-                } else {
-                    Toast.makeText(this, getResources().getString(R.string.pick_prompt), Toast.LENGTH_LONG).show();
+                    // View intent for images.
+                    Uri uri = getIntent().getData();
+                    boolean slideshow = getIntent().getBooleanExtra("slideshow", false);
+                    final SingleDataSource singleDataSource = new SingleDataSource(Gallery.this, uri.toString(), slideshow);
+                    final ConcatenatedDataSource singleCombinedDataSource = new ConcatenatedDataSource(singleDataSource, picasaDataSource);
+                    mGridLayer.setDataSource(singleCombinedDataSource);
+                    mGridLayer.setViewIntent(true, Utils.getBucketNameFromUri(uri));
+                    if (singleDataSource.isSingleImage()) {
+                        mGridLayer.setSingleImage(false);
+                    } else if (slideshow) {
+                        mGridLayer.setSingleImage(true);
+                        mGridLayer.startSlideshow();
+                    }
                 }
             }
-        } else {
-            // View intent for images.
-            Uri uri = getIntent().getData();
-            boolean slideshow = getIntent().getBooleanExtra("slideshow", false);
-            final SingleDataSource singleDataSource = new SingleDataSource(this, uri.toString(), slideshow);
-            final ConcatenatedDataSource singleCombinedDataSource = new ConcatenatedDataSource(singleDataSource, picasaDataSource);
-            mGridLayer.setDataSource(singleCombinedDataSource);
-            mGridLayer.setViewIntent(true, Utils.getBucketNameFromUri(uri));
-            if (singleDataSource.isSingleImage()) {
-                mGridLayer.setSingleImage(false);
-            } else if (slideshow) {
-                mGridLayer.setSingleImage(true);
-                mGridLayer.startSlideshow();
-            }
-        }
-        // We record the set of enabled accounts for picasa.
+        };
+        t.start();
+        //We record the set of enabled accounts for picasa.
         mAccountsEnabled = PicasaDataSource.getAccountStatus(this);
         Log.i(TAG, "onCreate");
     }
+    
+    private void showToast(final String string, final int duration) {
+        mHandler.post(new Runnable() {
+            public void run() {
+                Toast.makeText(Gallery.this, string, duration).show();
+            }
+        });
+    }
 
     public ReverseGeocoder getReverseGeocoder() {
         return mReverseGeocoder;
@@ -164,8 +189,21 @@ public final class Gallery extends Activity {
     @Override
     public void onResume() {
         super.onResume();
-        CacheService.computeDirtySets(this);
-        CacheService.startCache(this, false);
+        if (mDockSlideshow) {
+            if (mWakeLock != null) {
+                if (mWakeLock.isHeld()) {
+                    mWakeLock.release();
+                }
+            }
+            PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
+            mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "GridView.Slideshow.All");
+            mWakeLock.acquire();
+            return;
+        }
+        if (ImageManager.hasStorage()) {
+            CacheService.computeDirtySets(this);
+            CacheService.startCache(this, false);
+        }
         if (mRenderView != null) {
             mRenderView.onResume();
         }
@@ -201,6 +239,12 @@ public final class Gallery extends Activity {
         super.onPause();
         if (mRenderView != null)
             mRenderView.onPause();
+        if (mWakeLock != null) {
+            if (mWakeLock.isHeld()) {
+                mWakeLock.release();
+            }
+            mWakeLock = null;
+        }
         mPause = true;
     }
 
@@ -233,12 +277,6 @@ public final class Gallery extends Activity {
             }
             mGridLayer.shutdown();
         }
-        if (mWakeLock != null) {
-            if (mWakeLock.isHeld()) {
-                mWakeLock.release();
-            }
-            mWakeLock = null;
-        }
         if (mReverseGeocoder != null)
             mReverseGeocoder.shutdown();
         if (mRenderView != null) {
index 62bd1d3..f526532 100644 (file)
@@ -8,7 +8,8 @@ import android.view.GestureDetector;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 
-public final class GridInputProcessor implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener {
+public final class GridInputProcessor implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener,
+        ScaleGestureDetector.OnScaleGestureListener {
     private int mCurrentFocusSlot;
     private boolean mCurrentFocusIsPressed;
     private int mCurrentSelectedSlot;
@@ -37,6 +38,8 @@ public final class GridInputProcessor implements GestureDetector.OnGestureListen
     private boolean mPrevHitEdge;
     private boolean mTouchFeedbackDelivered;
     private GestureDetector mGestureDetector;
+    private ScaleGestureDetector mScaleGestureDetector;
+    private boolean mZoomGesture;
 
     public GridInputProcessor(Context context, GridCamera camera, GridLayer layer, RenderView view, Pool<Vector3f> pool,
             DisplayItem[] displayItems) {
@@ -48,7 +51,9 @@ public final class GridInputProcessor implements GestureDetector.OnGestureListen
         mContext = context;
         mDisplayItems = displayItems;
         mGestureDetector = new GestureDetector(context, this);
+        mScaleGestureDetector = new ScaleGestureDetector(context, this);
         mGestureDetector.setIsLongpressEnabled(true);
+        mZoomGesture = false;
     }
 
     public int getCurrentFocusSlot() {
@@ -125,6 +130,7 @@ public final class GridInputProcessor implements GestureDetector.OnGestureListen
             break;
         }
         mGestureDetector.onTouchEvent(event);
+        mScaleGestureDetector.onTouchEvent(event);
         return true;
     }
 
@@ -263,7 +269,7 @@ public final class GridInputProcessor implements GestureDetector.OnGestureListen
     }
 
     private void touchMoved(int posX, int posY, float timeElapsedx) {
-        if (mProcessTouch) {
+        if (mProcessTouch && !mZoomGesture) {
             GridLayer layer = mLayer;
             GridCamera camera = mCamera;
             float deltaX = -(posX - mPrevTouchPosX); // negation since the wall
@@ -347,12 +353,14 @@ public final class GridInputProcessor implements GestureDetector.OnGestureListen
     }
 
     private void touchEnded(int posX, int posY, float timeElapsedx) {
-        if (mProcessTouch == false)
+        if (mProcessTouch == false) {
+            mZoomGesture = false;
             return;
+        }
         int maxPixelsBeforeSwitch = mCamera.mWidth / 8;
         mCamera.mConvergenceSpeed = 2.0f;
         GridLayer layer = mLayer;
-        if (layer.getExpandedSlot() == Shared.INVALID && !layer.feedAboutToChange()) {
+        if (layer.getExpandedSlot() == Shared.INVALID && !layer.feedAboutToChange() && !mZoomGesture) {
             if (mCurrentSelectedSlot != Shared.INVALID) {
                 if (layer.getState() == GridLayer.STATE_FULL_SCREEN) {
                     if (!mTouchMoved) {
@@ -415,6 +423,7 @@ public final class GridInputProcessor implements GestureDetector.OnGestureListen
         mPrevTouchPosX = posX;
         mPrevTouchPosY = posY;
         mProcessTouch = false;
+        mZoomGesture = false;
     }
 
     private void constrainCamera(boolean b) {
@@ -672,4 +681,43 @@ public final class GridInputProcessor implements GestureDetector.OnGestureListen
     private void vibrateLong() {
         // mView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
     }
+
+    public boolean onScale(ScaleGestureDetector detector) {
+        final GridLayer layer = mLayer;
+        if (layer.getState() == GridLayer.STATE_FULL_SCREEN) {
+            float scale = detector.getScaleFactor();
+            float currentScale = layer.getZoomValue();
+            if (currentScale < 0.7f && scale < 1.0f) {
+                scale = 1.0f;
+            }
+            if (currentScale > 8.0f && scale > 1.0f) {
+                scale = 1.0f;
+            }
+            layer.setZoomValue(currentScale * scale);
+        }
+        return true;
+    }
+
+    public boolean onScaleBegin(ScaleGestureDetector detector) {
+        mZoomGesture = true;
+        mLayer.getHud().hideZoomButtons(true);
+        return true;
+    }
+    
+    public void onScaleEnd(ScaleGestureDetector detector) {
+        final GridLayer layer = mLayer;
+        if (layer.getState() == GridLayer.STATE_FULL_SCREEN) {
+            float currentScale = layer.getZoomValue();
+            if (currentScale < 1.0f) {
+                currentScale = 1.0f;
+            } else if (currentScale > 6.0f) {
+                currentScale = 6.0f;
+            }
+            if (currentScale != layer.getZoomValue()) {
+                layer.setZoomValue(currentScale);
+            }
+            layer.constrainCameraForSlot(mCurrentSelectedSlot);
+            mLayer.getHud().hideZoomButtons(false);
+        }
+    }
 }
index 5ab3ac4..fe804a2 100644 (file)
@@ -778,9 +778,11 @@ public final class GridLayer extends RootLayer implements MediaFeed.Listener, Ti
     public void renderBlended(RenderView view, GL11 gl) {
         // We draw the placeholder for all visible slots.
         if (mHud != null && mDrawManager != null) {
-            mDrawManager.drawBlendedComponents(view, gl, mSelectedAlpha, mState, mHud.getMode(), mTimeElapsedSinceStackViewReady,
-                    mTimeElapsedSinceGridViewReady, sSelectedBucketList, sMarkedBucketList, mMediaFeed.getWaitingForMediaScanner()
-                            || mFeedAboutToChange || mMediaFeed.isLoading());
+            if (mMediaFeed != null) {
+                mDrawManager.drawBlendedComponents(view, gl, mSelectedAlpha, mState, mHud.getMode(),
+                        mTimeElapsedSinceStackViewReady, mTimeElapsedSinceGridViewReady, sSelectedBucketList, sMarkedBucketList,
+                        mMediaFeed.getWaitingForMediaScanner() || mFeedAboutToChange || mMediaFeed.isLoading());
+            }
         }
     }
 
index cca6d0f..2f47321 100644 (file)
@@ -789,4 +789,9 @@ public final class HudLayer extends Layer {
     public Layer getMenuBar() {
         return mFullscreenMenu;
     }
+
+    public void hideZoomButtons(boolean hide) {
+        mZoomInButton.setHidden(hide);
+        mZoomOutButton.setHidden(hide);
+    }
 }
diff --git a/src/com/cooliris/media/ScaleGestureDetector.java b/src/com/cooliris/media/ScaleGestureDetector.java
new file mode 100644 (file)
index 0000000..117783b
--- /dev/null
@@ -0,0 +1,361 @@
+/*
+ * 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.cooliris.media;
+
+import android.content.Context;
+import android.view.MotionEvent;
+
+/**
+ * Detects transformation gestures involving more than one pointer ("multitouch")
+ * using the supplied {@link MotionEvent}s. The {@link OnScaleGestureListener}
+ * callback will notify users when a particular gesture event has occurred.
+ * This class should only be used with {@link MotionEvent}s reported via touch.
+ * 
+ * To use this class:
+ * <ul>
+ *  <li>Create an instance of the {@code ScaleGestureDetector} for your
+ *      {@link View}
+ *  <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
+ *          {@link #onTouchEvent(MotionEvent)}. The methods defined in your
+ *          callback will be executed when the events occur.
+ * </ul>
+ * @hide Pending API approval
+ */
+public class ScaleGestureDetector {
+    /**
+     * The listener for receiving notifications when gestures occur.
+     * If you want to listen for all the different gestures then implement
+     * this interface. If you only want to listen for a subset it might
+     * be easier to extend {@link SimpleOnScaleGestureListener}.
+     * 
+     * An application will receive events in the following order:
+     * <ul>
+     *  <li>One {@link OnScaleGestureListener#onScaleBegin()}
+     *  <li>Zero or more {@link OnScaleGestureListener#onScale()}
+     *  <li>One {@link OnScaleGestureListener#onTransformEnd()}
+     * </ul>
+     */
+    public interface OnScaleGestureListener {
+        /**
+         * Responds to scaling events for a gesture in progress.
+         * Reported by pointer motion.
+         * 
+         * @param detector The detector reporting the event - use this to
+         *          retrieve extended info about event state.
+         * @return Whether or not the detector should consider this event
+         *          as handled. If an event was not handled, the detector
+         *          will continue to accumulate movement until an event is
+         *          handled. This can be useful if an application, for example,
+         *          only wants to update scaling factors if the change is
+         *          greater than 0.01.
+         */
+        public boolean onScale(ScaleGestureDetector detector);
+
+        /**
+         * Responds to the beginning of a scaling gesture. Reported by
+         * new pointers going down.
+         * 
+         * @param detector The detector reporting the event - use this to
+         *          retrieve extended info about event state.
+         * @return Whether or not the detector should continue recognizing
+         *          this gesture. For example, if a gesture is beginning
+         *          with a focal point outside of a region where it makes
+         *          sense, onScaleBegin() may return false to ignore the
+         *          rest of the gesture.
+         */
+        public boolean onScaleBegin(ScaleGestureDetector detector);
+
+        /**
+         * Responds to the end of a scale gesture. Reported by existing
+         * pointers going up. If the end of a gesture would result in a fling,
+         * {@link onTransformFling()} is called instead.
+         * 
+         * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()}
+         * and {@link ScaleGestureDetector#getFocusY()} will return the location
+         * of the pointer remaining on the screen.
+         * 
+         * @param detector The detector reporting the event - use this to
+         *          retrieve extended info about event state.
+         */
+        public void onScaleEnd(ScaleGestureDetector detector);
+    }
+    
+    /**
+     * A convenience class to extend when you only want to listen for a subset
+     * of scaling-related events. This implements all methods in
+     * {@link OnScaleGestureListener} but does nothing.
+     * {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} and
+     * {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} return
+     * {@code true}. 
+     */
+    public class SimpleOnScaleGestureListener implements OnScaleGestureListener {
+
+        public boolean onScale(ScaleGestureDetector detector) {
+            return true;
+        }
+
+        public boolean onScaleBegin(ScaleGestureDetector detector) {
+            return true;
+        }
+
+        public void onScaleEnd(ScaleGestureDetector detector) {
+            // Intentionally empty
+        }
+    }
+
+    private static final float PRESSURE_THRESHOLD = 0.67f;
+
+    private Context mContext;
+    private OnScaleGestureListener mListener;
+    private boolean mGestureInProgress;
+
+    private MotionEvent mPrevEvent;
+    private MotionEvent mCurrEvent;
+
+    private float mFocusX;
+    private float mFocusY;
+    private float mPrevFingerDiffX;
+    private float mPrevFingerDiffY;
+    private float mCurrFingerDiffX;
+    private float mCurrFingerDiffY;
+    private float mCurrLen;
+    private float mPrevLen;
+    private float mScaleFactor;
+    private float mCurrPressure;
+    private float mPrevPressure;
+    private long mTimeDelta;
+
+    public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
+        mContext = context;
+        mListener = listener;
+    }
+
+    public boolean onTouchEvent(MotionEvent event) {
+        final int action = event.getAction();
+        boolean handled = true;
+
+        if (!mGestureInProgress) {
+            if ((action == MotionEvent.ACTION_POINTER_1_DOWN ||
+                    action == MotionEvent.ACTION_POINTER_2_DOWN) &&
+                    event.getPointerCount() >= 2) {
+                // We have a new multi-finger gesture
+                
+                // Be paranoid in case we missed an event
+                reset();
+                
+                mPrevEvent = MotionEvent.obtain(event);
+                mTimeDelta = 0;
+                
+                setContext(event);
+                mGestureInProgress = mListener.onScaleBegin(this);
+            }
+        } else {
+            // Transform gesture in progress - attempt to handle it
+            switch (action) {
+                case MotionEvent.ACTION_POINTER_1_UP:
+                case MotionEvent.ACTION_POINTER_2_UP:
+                    // Gesture ended
+                    setContext(event);
+                    
+                    // Set focus point to the remaining finger
+                    int id = (((action & MotionEvent.ACTION_POINTER_ID_MASK)
+                            >> MotionEvent.ACTION_POINTER_ID_SHIFT) == 0) ? 1 : 0;
+                    mFocusX = event.getX(id);
+                    mFocusY = event.getY(id);
+                    
+                    mListener.onScaleEnd(this);
+                    mGestureInProgress = false;
+
+                    reset();
+                    break;
+
+                case MotionEvent.ACTION_CANCEL:
+                    mListener.onScaleEnd(this);
+                    mGestureInProgress = false;
+
+                    reset();
+                    break;
+
+                case MotionEvent.ACTION_MOVE:
+                    setContext(event);
+
+                    // Only accept the event if our relative pressure is within
+                    // a certain limit - this can help filter shaky data as a
+                    // finger is lifted.
+                    if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) {
+                        final boolean updatePrevious = mListener.onScale(this);
+
+                        if (updatePrevious) {
+                            mPrevEvent.recycle();
+                            mPrevEvent = MotionEvent.obtain(event);
+                        }
+                    }
+                    break;
+            }
+        }
+        return handled;
+    }
+
+    private void setContext(MotionEvent curr) {
+        if (mCurrEvent != null) {
+            mCurrEvent.recycle();
+        }
+        mCurrEvent = MotionEvent.obtain(curr);
+
+        mCurrLen = -1;
+        mPrevLen = -1;
+        mScaleFactor = -1;
+
+        final MotionEvent prev = mPrevEvent;
+
+        final float px0 = prev.getX(0);
+        final float py0 = prev.getY(0);
+        final float px1 = prev.getX(1);
+        final float py1 = prev.getY(1);
+        final float cx0 = curr.getX(0);
+        final float cy0 = curr.getY(0);
+        final float cx1 = curr.getX(1);
+        final float cy1 = curr.getY(1);
+
+        final float pvx = px1 - px0;
+        final float pvy = py1 - py0;
+        final float cvx = cx1 - cx0;
+        final float cvy = cy1 - cy0;
+        mPrevFingerDiffX = pvx;
+        mPrevFingerDiffY = pvy;
+        mCurrFingerDiffX = cvx;
+        mCurrFingerDiffY = cvy;
+
+        mFocusX = cx0 + cvx * 0.5f;
+        mFocusY = cy0 + cvy * 0.5f;
+        mTimeDelta = curr.getEventTime() - prev.getEventTime();
+        mCurrPressure = curr.getPressure(0) + curr.getPressure(1);
+        mPrevPressure = prev.getPressure(0) + prev.getPressure(1);
+    }
+
+    private void reset() {
+        if (mPrevEvent != null) {
+            mPrevEvent.recycle();
+            mPrevEvent = null;
+        }
+        if (mCurrEvent != null) {
+            mCurrEvent.recycle();
+            mCurrEvent = null;
+        }
+    }
+
+    /**
+     * Returns {@code true} if a two-finger scale gesture is in progress.
+     * @return {@code true} if a scale gesture is in progress, {@code false} otherwise.
+     */
+    public boolean isInProgress() {
+        return mGestureInProgress;
+    }
+
+    /**
+     * Get the X coordinate of the current gesture's focal point.
+     * If a gesture is in progress, the focal point is directly between
+     * the two pointers forming the gesture.
+     * If a gesture is ending, the focal point is the location of the
+     * remaining pointer on the screen.
+     * If {@link isInProgress()} would return false, the result of this
+     * function is undefined.
+     * 
+     * @return X coordinate of the focal point in pixels.
+     */
+    public float getFocusX() {
+        return mFocusX;
+    }
+
+    /**
+     * Get the Y coordinate of the current gesture's focal point.
+     * If a gesture is in progress, the focal point is directly between
+     * the two pointers forming the gesture.
+     * If a gesture is ending, the focal point is the location of the
+     * remaining pointer on the screen.
+     * If {@link isInProgress()} would return false, the result of this
+     * function is undefined.
+     * 
+     * @return Y coordinate of the focal point in pixels.
+     */
+    public float getFocusY() {
+        return mFocusY;
+    }
+
+    /**
+     * Return the current distance between the two pointers forming the
+     * gesture in progress.
+     * 
+     * @return Distance between pointers in pixels.
+     */
+    public float getCurrentSpan() {
+        if (mCurrLen == -1) {
+            final float cvx = mCurrFingerDiffX;
+            final float cvy = mCurrFingerDiffY;
+            mCurrLen = (float)Math.sqrt(cvx*cvx + cvy*cvy);
+        }
+        return mCurrLen;
+    }
+
+    /**
+     * Return the previous distance between the two pointers forming the
+     * gesture in progress.
+     * 
+     * @return Previous distance between pointers in pixels.
+     */
+    public float getPreviousSpan() {
+        if (mPrevLen == -1) {
+            final float pvx = mPrevFingerDiffX;
+            final float pvy = mPrevFingerDiffY;
+            mPrevLen = (float)Math.sqrt(pvx*pvx + pvy*pvy);
+        }
+        return mPrevLen;
+    }
+
+    /**
+     * Return the scaling factor from the previous scale event to the current
+     * event. This value is defined as
+     * ({@link getCurrentSpan()} / {@link getPreviousSpan()}).
+     * 
+     * @return The current scaling factor.
+     */
+    public float getScaleFactor() {
+        if (mScaleFactor == -1) {
+            mScaleFactor = getCurrentSpan() / getPreviousSpan();
+        }
+        return mScaleFactor;
+    }
+    
+    /**
+     * Return the time difference in milliseconds between the previous
+     * accepted scaling event and the current scaling event.
+     * 
+     * @return Time difference since the last scaling event in milliseconds.
+     */
+    public long getTimeDelta() {
+        return mTimeDelta;
+    }
+    
+    /**
+     * Return the event time of the current event being processed.
+     * 
+     * @return Current event time in milliseconds.
+     */
+    public long getEventTime() {
+        return mCurrEvent.getEventTime();
+    }
+}
index f9853ac..403a54a 100644 (file)
@@ -427,8 +427,8 @@ public final class PicasaContentProvider extends TableContentProvider {
 
         // Mark that photos changed.
         // context.photosChanged = true;
-        getContext().getContentResolver().notifyChange(ALBUMS_URI, null);
-        getContext().getContentResolver().notifyChange(PHOTOS_URI, null);
+        getContext().getContentResolver().notifyChange(ALBUMS_URI, null, false);
+        getContext().getContentResolver().notifyChange(PHOTOS_URI, null, false);
     }
 
     private void deleteUser(SQLiteDatabase db, String account) {
@@ -523,10 +523,10 @@ public final class PicasaContentProvider extends TableContentProvider {
             // Send notifications if needed and reset state.
             ContentResolver cr = getContext().getContentResolver();
             if (albumsChanged) {
-                cr.notifyChange(ALBUMS_URI, null);
+                cr.notifyChange(ALBUMS_URI, null, false);
             }
             if (photosChanged) {
-                cr.notifyChange(PHOTOS_URI, null);
+                cr.notifyChange(PHOTOS_URI, null, false);
             }
             albumsChanged = false;
             photosChanged = false;
index d6b4e67..23b7285 100644 (file)
@@ -1,14 +1,19 @@
 package com.cooliris.picasa;
 
+import java.io.IOException;
+
 import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
 import android.content.AbstractThreadedSyncAdapter;
 import android.content.BroadcastReceiver;
 import android.content.ContentProviderClient;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SyncResult;
 import android.os.Bundle;
-import android.util.Log;
 
 public class PicasaSyncAdapter extends AbstractThreadedSyncAdapter {
     private final Context mContext;
@@ -22,6 +27,33 @@ public class PicasaSyncAdapter extends AbstractThreadedSyncAdapter {
     @Override
     public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient providerClient,
             SyncResult syncResult) {
+        if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false)) {
+            try {
+                Account[] picasaAccounts = AccountManager.get(getContext())
+                        .getAccountsByTypeAndFeatures(
+                        PicasaService.ACCOUNT_TYPE,
+                        new String[] { PicasaService.FEATURE_SERVICE_NAME },
+                        null /* callback */, null /* handler */).getResult();
+                boolean isPicasaAccount = false;
+                for (Account picasaAccount : picasaAccounts) {
+                    if (account.equals(picasaAccount)) {
+                        isPicasaAccount = true;
+                        break;
+                    }
+                }
+                if (isPicasaAccount) {
+                    ContentResolver.setIsSyncable(account, authority, 1);
+                    ContentResolver.setSyncAutomatically(account, authority, true);
+                }
+            } catch (OperationCanceledException e) {
+                ;
+            } catch (IOException e) {
+                ;
+            } catch (AuthenticatorException e) {
+                ;
+            }
+            return;
+        }
         PicasaService.performSync(mContext, account, extras, syncResult);
     }