1f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin/*
2f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin * Copyright (C) 2010 The Android Open Source Project
3f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin *
4f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin * Licensed under the Apache License, Version 2.0 (the "License");
5f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin * you may not use this file except in compliance with the License.
6f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin * You may obtain a copy of the License at
7f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin *
8f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin *      http://www.apache.org/licenses/LICENSE-2.0
9f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin *
10f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin * Unless required by applicable law or agreed to in writing, software
11f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin * distributed under the License is distributed on an "AS IS" BASIS,
12f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin * See the License for the specific language governing permissions and
14f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin * limitations under the License.
15f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin */
16f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
17f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Linpackage com.android.gallery3d.data;
18f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
19f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Linimport android.content.Context;
20f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Linimport android.text.format.DateFormat;
21f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Linimport android.text.format.DateUtils;
22f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
232b3ee0ea07246b859a5b75d8a6102a7cce7ec838Owen Linimport com.android.gallery3d.common.Utils;
242b3ee0ea07246b859a5b75d8a6102a7cce7ec838Owen Linimport com.android.gallery3d.util.GalleryUtils;
252b3ee0ea07246b859a5b75d8a6102a7cce7ec838Owen Lin
26f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Linimport java.util.ArrayList;
27f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Linimport java.util.Collections;
28f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Linimport java.util.Comparator;
29f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
30f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Linpublic class TimeClustering extends Clustering {
317817979db0c52ffeacb951625b1e821eba303285Ahbong Chang    @SuppressWarnings("unused")
32f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private static final String TAG = "TimeClustering";
33f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
34f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    // If 2 items are greater than 25 miles apart, they will be in different
35f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    // clusters.
36f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private static final int GEOGRAPHIC_DISTANCE_CUTOFF_IN_MILES = 20;
37f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
38f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    // Do not want to split based on anything under 1 min.
39f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private static final long MIN_CLUSTER_SPLIT_TIME_IN_MS = 60000L;
40f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
41f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    // Disregard a cluster split time of anything over 2 hours.
42f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private static final long MAX_CLUSTER_SPLIT_TIME_IN_MS = 7200000L;
43f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
44f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    // Try and get around 9 clusters (best-effort for the common case).
45f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private static final int NUM_CLUSTERS_TARGETED = 9;
46f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
47f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    // Try and merge 2 clusters if they are both smaller than min cluster size.
48f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    // The min cluster size can range from 8 to 15.
49f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private static final int MIN_MIN_CLUSTER_SIZE = 8;
50f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private static final int MAX_MIN_CLUSTER_SIZE = 15;
51f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
52f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    // Try and split a cluster if it is bigger than max cluster size.
53f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    // The max cluster size can range from 20 to 50.
54f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private static final int MIN_MAX_CLUSTER_SIZE = 20;
55f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private static final int MAX_MAX_CLUSTER_SIZE = 50;
56f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
57f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    // Initially put 2 items in the same cluster as long as they are within
58f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    // 3 cluster frequencies of each other.
59f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private static int CLUSTER_SPLIT_MULTIPLIER = 3;
60f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
61f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    // The minimum change factor in the time between items to consider a
62f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    // partition.
63f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    // Example: (Item 3 - Item 2) / (Item 2 - Item 1).
64f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private static final int MIN_PARTITION_CHANGE_FACTOR = 2;
65f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
66f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    // Make the cluster split time of a large cluster half that of a regular
67f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    // cluster.
68f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private static final int PARTITION_CLUSTER_SPLIT_TIME_FACTOR = 2;
69f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
70f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private Context mContext;
71f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private ArrayList<Cluster> mClusters;
72f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private String[] mNames;
73f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private Cluster mCurrCluster;
74f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
75f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private long mClusterSplitTime =
76f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            (MIN_CLUSTER_SPLIT_TIME_IN_MS + MAX_CLUSTER_SPLIT_TIME_IN_MS) / 2;
77f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private long mLargeClusterSplitTime =
78f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            mClusterSplitTime / PARTITION_CLUSTER_SPLIT_TIME_FACTOR;
79f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private int mMinClusterSize = (MIN_MIN_CLUSTER_SIZE + MAX_MIN_CLUSTER_SIZE) / 2;
80f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private int mMaxClusterSize = (MIN_MAX_CLUSTER_SIZE + MAX_MAX_CLUSTER_SIZE) / 2;
81f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
82f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
83f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private static final Comparator<SmallItem> sDateComparator =
84f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            new DateComparator();
85f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
86f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private static class DateComparator implements Comparator<SmallItem> {
877817979db0c52ffeacb951625b1e821eba303285Ahbong Chang        @Override
88f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        public int compare(SmallItem item1, SmallItem item2) {
89f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            return -Utils.compare(item1.dateInMs, item2.dateInMs);
90f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        }
91f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    }
92f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
93f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    public TimeClustering(Context context) {
94f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        mContext = context;
95f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        mClusters = new ArrayList<Cluster>();
96f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        mCurrCluster = new Cluster();
97f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    }
98f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
99f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    @Override
100f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    public void run(MediaSet baseSet) {
101f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        final int total = baseSet.getTotalMediaItemCount();
102f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        final SmallItem[] buf = new SmallItem[total];
103f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        final double[] latLng = new double[2];
104f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
105f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
1067817979db0c52ffeacb951625b1e821eba303285Ahbong Chang            @Override
107f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            public void consume(int index, MediaItem item) {
108f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                if (index < 0 || index >= total) return;
109f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                SmallItem s = new SmallItem();
110f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                s.path = item.getPath();
111f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                s.dateInMs = item.getDateInMs();
112f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                item.getLatLong(latLng);
113f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                s.lat = latLng[0];
114f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                s.lng = latLng[1];
115f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                buf[index] = s;
116f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            }
117f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        });
118f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
119f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        ArrayList<SmallItem> items = new ArrayList<SmallItem>(total);
120f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        for (int i = 0; i < total; i++) {
121f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            if (buf[i] != null) {
122f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                items.add(buf[i]);
123f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            }
124f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        }
125f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
126f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        Collections.sort(items, sDateComparator);
127f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
128f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        int n = items.size();
129f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        long minTime = 0;
130f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        long maxTime = 0;
131f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        for (int i = 0; i < n; i++) {
132f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            long t = items.get(i).dateInMs;
133f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            if (t == 0) continue;
134f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            if (minTime == 0) {
135f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                minTime = maxTime = t;
136f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            } else {
137f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                minTime = Math.min(minTime, t);
138f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                maxTime = Math.max(maxTime, t);
139f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            }
140f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        }
141f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
142f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        setTimeRange(maxTime - minTime, n);
143f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
144f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        for (int i = 0; i < n; i++) {
145f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            compute(items.get(i));
146f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        }
147f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
148f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        compute(null);
149f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
150f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        int m = mClusters.size();
151f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        mNames = new String[m];
152f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        for (int i = 0; i < m; i++) {
153f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            mNames[i] = mClusters.get(i).generateCaption(mContext);
154f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        }
155f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    }
156f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
157f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    @Override
158f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    public int getNumberOfClusters() {
159f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        return mClusters.size();
160f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    }
161f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
162f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    @Override
163f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    public ArrayList<Path> getCluster(int index) {
164f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        ArrayList<SmallItem> items = mClusters.get(index).getItems();
165f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        ArrayList<Path> result = new ArrayList<Path>(items.size());
166f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        for (int i = 0, n = items.size(); i < n; i++) {
167f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            result.add(items.get(i).path);
168f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        }
169f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        return result;
170f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    }
171f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
172f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    @Override
173f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    public String getClusterName(int index) {
174f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        return mNames[index];
175f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    }
176f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
177f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private void setTimeRange(long timeRange, int numItems) {
178f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        if (numItems != 0) {
179f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            int meanItemsPerCluster = numItems / NUM_CLUSTERS_TARGETED;
180f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            // Heuristic to get min and max cluster size - half and double the
181f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            // desired items per cluster.
182f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            mMinClusterSize = meanItemsPerCluster / 2;
183f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            mMaxClusterSize = meanItemsPerCluster * 2;
184f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            mClusterSplitTime = timeRange / numItems * CLUSTER_SPLIT_MULTIPLIER;
185f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        }
186f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        mClusterSplitTime = Utils.clamp(mClusterSplitTime, MIN_CLUSTER_SPLIT_TIME_IN_MS, MAX_CLUSTER_SPLIT_TIME_IN_MS);
187f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        mLargeClusterSplitTime = mClusterSplitTime / PARTITION_CLUSTER_SPLIT_TIME_FACTOR;
188f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        mMinClusterSize = Utils.clamp(mMinClusterSize, MIN_MIN_CLUSTER_SIZE, MAX_MIN_CLUSTER_SIZE);
189f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        mMaxClusterSize = Utils.clamp(mMaxClusterSize, MIN_MAX_CLUSTER_SIZE, MAX_MAX_CLUSTER_SIZE);
190f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    }
191f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
192f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private void compute(SmallItem currentItem) {
193f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        if (currentItem != null) {
194f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            int numClusters = mClusters.size();
195f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            int numCurrClusterItems = mCurrCluster.size();
196f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            boolean geographicallySeparateItem = false;
197f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            boolean itemAddedToCurrentCluster = false;
198f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
199f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            // Determine if this item should go in the current cluster or be the
200f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            // start of a new cluster.
201f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            if (numCurrClusterItems == 0) {
202f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                mCurrCluster.addItem(currentItem);
203f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            } else {
204f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                SmallItem prevItem = mCurrCluster.getLastItem();
205f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                if (isGeographicallySeparated(prevItem, currentItem)) {
206f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    mClusters.add(mCurrCluster);
207f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    geographicallySeparateItem = true;
208f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                } else if (numCurrClusterItems > mMaxClusterSize) {
209f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    splitAndAddCurrentCluster();
210f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                } else if (timeDistance(prevItem, currentItem) < mClusterSplitTime) {
211f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    mCurrCluster.addItem(currentItem);
212f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    itemAddedToCurrentCluster = true;
213f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                } else if (numClusters > 0 && numCurrClusterItems < mMinClusterSize
214f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                        && !mCurrCluster.mGeographicallySeparatedFromPrevCluster) {
215f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    mergeAndAddCurrentCluster();
216f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                } else {
217f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    mClusters.add(mCurrCluster);
218f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                }
219f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
220f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                // Creating a new cluster and adding the current item to it.
221f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                if (!itemAddedToCurrentCluster) {
222f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    mCurrCluster = new Cluster();
223f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    if (geographicallySeparateItem) {
224f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                        mCurrCluster.mGeographicallySeparatedFromPrevCluster = true;
225f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    }
226f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    mCurrCluster.addItem(currentItem);
227f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                }
228f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            }
229f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        } else {
230f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            if (mCurrCluster.size() > 0) {
231f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                int numClusters = mClusters.size();
232f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                int numCurrClusterItems = mCurrCluster.size();
233f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
234f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                // The last cluster may potentially be too big or too small.
235f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                if (numCurrClusterItems > mMaxClusterSize) {
236f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    splitAndAddCurrentCluster();
237f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                } else if (numClusters > 0 && numCurrClusterItems < mMinClusterSize
238f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                        && !mCurrCluster.mGeographicallySeparatedFromPrevCluster) {
239f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    mergeAndAddCurrentCluster();
240f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                } else {
241f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    mClusters.add(mCurrCluster);
242f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                }
243f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                mCurrCluster = new Cluster();
244f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            }
245f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        }
246f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    }
247f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
248f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private void splitAndAddCurrentCluster() {
249f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        ArrayList<SmallItem> currClusterItems = mCurrCluster.getItems();
250f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        int numCurrClusterItems = mCurrCluster.size();
251f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        int secondPartitionStartIndex = getPartitionIndexForCurrentCluster();
252f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        if (secondPartitionStartIndex != -1) {
253f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            Cluster partitionedCluster = new Cluster();
254f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            for (int j = 0; j < secondPartitionStartIndex; j++) {
255f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                partitionedCluster.addItem(currClusterItems.get(j));
256f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            }
257f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            mClusters.add(partitionedCluster);
258f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            partitionedCluster = new Cluster();
259f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            for (int j = secondPartitionStartIndex; j < numCurrClusterItems; j++) {
260f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                partitionedCluster.addItem(currClusterItems.get(j));
261f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            }
262f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            mClusters.add(partitionedCluster);
263f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        } else {
264f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            mClusters.add(mCurrCluster);
265f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        }
266f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    }
267f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
268f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private int getPartitionIndexForCurrentCluster() {
269f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        int partitionIndex = -1;
270f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        float largestChange = MIN_PARTITION_CHANGE_FACTOR;
271f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        ArrayList<SmallItem> currClusterItems = mCurrCluster.getItems();
272f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        int numCurrClusterItems = mCurrCluster.size();
273f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        int minClusterSize = mMinClusterSize;
274f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
275f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        // Could be slightly more efficient here but this code seems cleaner.
276f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        if (numCurrClusterItems > minClusterSize + 1) {
277f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            for (int i = minClusterSize; i < numCurrClusterItems - minClusterSize; i++) {
278f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                SmallItem prevItem = currClusterItems.get(i - 1);
279f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                SmallItem currItem = currClusterItems.get(i);
280f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                SmallItem nextItem = currClusterItems.get(i + 1);
281f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
282f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                long timeNext = nextItem.dateInMs;
283f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                long timeCurr = currItem.dateInMs;
284f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                long timePrev = prevItem.dateInMs;
285f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
286f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                if (timeNext == 0 || timeCurr == 0 || timePrev == 0) continue;
287f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
288f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                long diff1 = Math.abs(timeNext - timeCurr);
289f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                long diff2 = Math.abs(timeCurr - timePrev);
290f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
291f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                float change = Math.max(diff1 / (diff2 + 0.01f), diff2 / (diff1 + 0.01f));
292f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                if (change > largestChange) {
293f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    if (timeDistance(currItem, prevItem) > mLargeClusterSplitTime) {
294f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                        partitionIndex = i;
295f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                        largestChange = change;
296f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    } else if (timeDistance(nextItem, currItem) > mLargeClusterSplitTime) {
297f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                        partitionIndex = i + 1;
298f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                        largestChange = change;
299f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    }
300f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                }
301f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            }
302f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        }
303f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        return partitionIndex;
304f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    }
305f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
306f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private void mergeAndAddCurrentCluster() {
307f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        int numClusters = mClusters.size();
308f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        Cluster prevCluster = mClusters.get(numClusters - 1);
309f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        ArrayList<SmallItem> currClusterItems = mCurrCluster.getItems();
310f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        int numCurrClusterItems = mCurrCluster.size();
311f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        if (prevCluster.size() < mMinClusterSize) {
312f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            for (int i = 0; i < numCurrClusterItems; i++) {
313f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                prevCluster.addItem(currClusterItems.get(i));
314f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            }
315f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            mClusters.set(numClusters - 1, prevCluster);
316f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        } else {
317f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            mClusters.add(mCurrCluster);
318f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        }
319f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    }
320f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
321f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    // Returns true if a, b are sufficiently geographically separated.
322f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private static boolean isGeographicallySeparated(SmallItem itemA, SmallItem itemB) {
323f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        if (!GalleryUtils.isValidLocation(itemA.lat, itemA.lng)
324f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                || !GalleryUtils.isValidLocation(itemB.lat, itemB.lng)) {
325f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            return false;
326f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        }
327f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
328f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        double distance = GalleryUtils.fastDistanceMeters(
329f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            Math.toRadians(itemA.lat),
330f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            Math.toRadians(itemA.lng),
331f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            Math.toRadians(itemB.lat),
332f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            Math.toRadians(itemB.lng));
333f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        return (GalleryUtils.toMile(distance) > GEOGRAPHIC_DISTANCE_CUTOFF_IN_MILES);
334f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    }
335f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
336f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    // Returns the time interval between the two items in milliseconds.
337f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private static long timeDistance(SmallItem a, SmallItem b) {
338f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        return Math.abs(a.dateInMs - b.dateInMs);
339f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    }
340f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin}
341f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
342f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Linclass SmallItem {
343f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    Path path;
344f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    long dateInMs;
345f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    double lat, lng;
346f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin}
347f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
348f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Linclass Cluster {
349f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    @SuppressWarnings("unused")
350f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private static final String TAG = "Cluster";
351f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private static final String MMDDYY_FORMAT = "MMddyy";
352f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
353f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    // This is for TimeClustering only.
354f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    public boolean mGeographicallySeparatedFromPrevCluster = false;
355f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
356f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    private ArrayList<SmallItem> mItems = new ArrayList<SmallItem>();
357f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
358f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    public Cluster() {
359f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    }
360f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
361f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    public void addItem(SmallItem item) {
362f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        mItems.add(item);
363f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    }
364f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
365f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    public int size() {
366f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        return mItems.size();
367f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    }
368f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
369f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    public SmallItem getLastItem() {
370f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        int n = mItems.size();
371f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        return (n == 0) ? null : mItems.get(n - 1);
372f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    }
373f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
374f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    public ArrayList<SmallItem> getItems() {
375f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        return mItems;
376f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    }
377f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
378f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    public String generateCaption(Context context) {
379f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        int n = mItems.size();
380f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        long minTimestamp = 0;
381f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        long maxTimestamp = 0;
382f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
383f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        for (int i = 0; i < n; i++) {
384f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            long t = mItems.get(i).dateInMs;
385f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            if (t == 0) continue;
386f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            if (minTimestamp == 0) {
387f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                minTimestamp = maxTimestamp = t;
388f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            } else {
389f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                minTimestamp = Math.min(minTimestamp, t);
390f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                maxTimestamp = Math.max(maxTimestamp, t);
391f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            }
392f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        }
393f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        if (minTimestamp == 0) return "";
394f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
395f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        String caption;
396f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        String minDay = DateFormat.format(MMDDYY_FORMAT, minTimestamp)
397f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                .toString();
398f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        String maxDay = DateFormat.format(MMDDYY_FORMAT, maxTimestamp)
399f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                .toString();
400f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
401f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        if (minDay.substring(4).equals(maxDay.substring(4))) {
402f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            // The items are from the same year - show at least as
403f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            // much granularity as abbrev_all allows.
404f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            caption = DateUtils.formatDateRange(context, minTimestamp,
405f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    maxTimestamp, DateUtils.FORMAT_ABBREV_ALL);
406f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
407f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            // Get a more granular date range string if the min and
408f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            // max timestamp are on the same day and from the
409f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            // current year.
410f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            if (minDay.equals(maxDay)) {
411f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                int flags = DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_DATE;
412f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                // Contains the year only if the date does not
413f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                // correspond to the current year.
414f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                String dateRangeWithOptionalYear = DateUtils.formatDateTime(
415f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                        context, minTimestamp, flags);
416f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                String dateRangeWithYear = DateUtils.formatDateTime(
417f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                        context, minTimestamp, flags | DateUtils.FORMAT_SHOW_YEAR);
418f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                if (!dateRangeWithOptionalYear.equals(dateRangeWithYear)) {
419f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    // This means both dates are from the same year
420f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    // - show the time.
421f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    // Not enough room to display the time range.
422f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    // Pick the mid-point.
423f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    long midTimestamp = (minTimestamp + maxTimestamp) / 2;
424f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    caption = DateUtils.formatDateRange(context, midTimestamp,
425f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                            midTimestamp, DateUtils.FORMAT_SHOW_TIME | flags);
426f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                }
427f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            }
428f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        } else {
429f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            // The items are not from the same year - only show
430f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            // month and year.
431f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            int flags = DateUtils.FORMAT_NO_MONTH_DAY
432f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    | DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_DATE;
433f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin            caption = DateUtils.formatDateRange(context, minTimestamp,
434f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin                    maxTimestamp, flags);
435f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        }
436f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin
437f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin        return caption;
438f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin    }
439f9a0a4306d589b4a4e20554fed512a603426bfa1Owen Lin}
440