1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.gallery3d.data;
18
19import java.util.ArrayList;
20
21// FilterDeleteSet filters a base MediaSet to remove some deletion items (we
22// expect the number to be small). The user can use the following methods to
23// add/remove deletion items:
24//
25// void addDeletion(Path path, int index);
26// void removeDelection(Path path);
27// void clearDeletion();
28// int getNumberOfDeletions();
29//
30public class FilterDeleteSet extends MediaSet implements ContentListener {
31    @SuppressWarnings("unused")
32    private static final String TAG = "FilterDeleteSet";
33
34    private static final int REQUEST_ADD = 1;
35    private static final int REQUEST_REMOVE = 2;
36    private static final int REQUEST_CLEAR = 3;
37
38    private static class Request {
39        int type;  // one of the REQUEST_* constants
40        Path path;
41        int indexHint;
42        public Request(int type, Path path, int indexHint) {
43            this.type = type;
44            this.path = path;
45            this.indexHint = indexHint;
46        }
47    }
48
49    private static class Deletion {
50        Path path;
51        int index;
52        public Deletion(Path path, int index) {
53            this.path = path;
54            this.index = index;
55        }
56    }
57
58    // The underlying MediaSet
59    private final MediaSet mBaseSet;
60
61    // Pending Requests
62    private ArrayList<Request> mRequests = new ArrayList<Request>();
63
64    // Deletions currently in effect, ordered by index
65    private ArrayList<Deletion> mCurrent = new ArrayList<Deletion>();
66
67    public FilterDeleteSet(Path path, MediaSet baseSet) {
68        super(path, INVALID_DATA_VERSION);
69        mBaseSet = baseSet;
70        mBaseSet.addContentListener(this);
71    }
72
73    @Override
74    public boolean isCameraRoll() {
75        return mBaseSet.isCameraRoll();
76    }
77
78    @Override
79    public String getName() {
80        return mBaseSet.getName();
81    }
82
83    @Override
84    public int getMediaItemCount() {
85        return mBaseSet.getMediaItemCount() - mCurrent.size();
86    }
87
88    // Gets the MediaItems whose (post-deletion) index are in the range [start,
89    // start + count). Because we remove some of the MediaItems, the index need
90    // to be adjusted.
91    //
92    // For example, if there are 12 items in total. The deleted items are 3, 5,
93    // 10, and the the requested range is [3, 7]:
94    //
95    // The original index:   0 1 2 3 4 5 6 7 8 9 A B C
96    // The deleted items:          X   X         X
97    // The new index:        0 1 2   3   4 5 6 7   8 9
98    // Requested:                    *   * * * *
99    //
100    // We need to figure out the [3, 7] actually maps to the original index 4,
101    // 6, 7, 8, 9.
102    //
103    // We can break the MediaItems into segments, each segment other than the
104    // last one ends in a deleted item. The difference between the new index and
105    // the original index increases with each segment:
106    //
107    // 0 1 2 X     (new index = old index)
108    // 4 X         (new index = old index - 1)
109    // 6 7 8 9 X   (new index = old index - 2)
110    // B C         (new index = old index - 3)
111    //
112    @Override
113    public ArrayList<MediaItem> getMediaItem(int start, int count) {
114        if (count <= 0) return new ArrayList<MediaItem>();
115
116        int end = start + count - 1;
117        int n = mCurrent.size();
118        // Find the segment that "start" falls into. Count the number of items
119        // not yet deleted until it reaches "start".
120        int i = 0;
121        for (i = 0; i < n; i++) {
122            Deletion d = mCurrent.get(i);
123            if (d.index - i > start) break;
124        }
125        // Find the segment that "end" falls into.
126        int j = i;
127        for (; j < n; j++) {
128            Deletion d = mCurrent.get(j);
129            if (d.index - j > end) break;
130        }
131
132        // Now get enough to cover deleted items in [start, end]
133        ArrayList<MediaItem> base = mBaseSet.getMediaItem(start + i, count + (j - i));
134
135        // Remove the deleted items.
136        for (int m = j - 1; m >= i; m--) {
137            Deletion d = mCurrent.get(m);
138            int k = d.index - (start + i);
139            base.remove(k);
140        }
141        return base;
142    }
143
144    // We apply the pending requests in the mRequests to construct mCurrent in reload().
145    @Override
146    public long reload() {
147        boolean newData = mBaseSet.reload() > mDataVersion;
148        synchronized (mRequests) {
149            if (!newData && mRequests.isEmpty()) {
150                return mDataVersion;
151            }
152            for (int i = 0; i < mRequests.size(); i++) {
153                Request r = mRequests.get(i);
154                switch (r.type) {
155                    case REQUEST_ADD: {
156                        // Add the path into mCurrent if there is no duplicate.
157                        int n = mCurrent.size();
158                        int j;
159                        for (j = 0; j < n; j++) {
160                            if (mCurrent.get(j).path == r.path) break;
161                        }
162                        if (j == n) {
163                            mCurrent.add(new Deletion(r.path, r.indexHint));
164                        }
165                        break;
166                    }
167                    case REQUEST_REMOVE: {
168                        // Remove the path from mCurrent.
169                        int n = mCurrent.size();
170                        for (int j = 0; j < n; j++) {
171                            if (mCurrent.get(j).path == r.path) {
172                                mCurrent.remove(j);
173                                break;
174                            }
175                        }
176                        break;
177                    }
178                    case REQUEST_CLEAR: {
179                        mCurrent.clear();
180                        break;
181                    }
182                }
183            }
184            mRequests.clear();
185        }
186
187        if (!mCurrent.isEmpty()) {
188            // See if the elements in mCurrent can be found in the MediaSet. We
189            // don't want to search the whole mBaseSet, so we just search a
190            // small window that contains the index hints (plus some margin).
191            int minIndex = mCurrent.get(0).index;
192            int maxIndex = minIndex;
193            for (int i = 1; i < mCurrent.size(); i++) {
194                Deletion d = mCurrent.get(i);
195                minIndex = Math.min(d.index, minIndex);
196                maxIndex = Math.max(d.index, maxIndex);
197            }
198
199            int n = mBaseSet.getMediaItemCount();
200            int from = Math.max(minIndex - 5, 0);
201            int to = Math.min(maxIndex + 5, n);
202            ArrayList<MediaItem> items = mBaseSet.getMediaItem(from, to - from);
203            ArrayList<Deletion> result = new ArrayList<Deletion>();
204            for (int i = 0; i < items.size(); i++) {
205                MediaItem item = items.get(i);
206                if (item == null) continue;
207                Path p = item.getPath();
208                // Find the matching path in mCurrent, if found move it to result
209                for (int j = 0; j < mCurrent.size(); j++) {
210                    Deletion d = mCurrent.get(j);
211                    if (d.path == p) {
212                        d.index = from + i;
213                        result.add(d);
214                        mCurrent.remove(j);
215                        break;
216                    }
217                }
218            }
219            mCurrent = result;
220        }
221
222        mDataVersion = nextVersionNumber();
223        return mDataVersion;
224    }
225
226    private void sendRequest(int type, Path path, int indexHint) {
227        Request r = new Request(type, path, indexHint);
228        synchronized (mRequests) {
229            mRequests.add(r);
230        }
231        notifyContentChanged();
232    }
233
234    @Override
235    public void onContentDirty() {
236        notifyContentChanged();
237    }
238
239    public void addDeletion(Path path, int indexHint) {
240        sendRequest(REQUEST_ADD, path, indexHint);
241    }
242
243    public void removeDeletion(Path path) {
244        sendRequest(REQUEST_REMOVE, path, 0 /* unused */);
245    }
246
247    public void clearDeletion() {
248        sendRequest(REQUEST_CLEAR, null /* unused */ , 0 /* unused */);
249    }
250
251    // Returns number of deletions _in effect_ (the number will only gets
252    // updated after a reload()).
253    public int getNumberOfDeletions() {
254        return mCurrent.size();
255    }
256}
257