1/*
2 * Copyright (C) 2013 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.example.android.apis.content;
18
19import android.app.Activity;
20import android.app.LoaderManager;
21import android.content.BroadcastReceiver;
22import android.content.Context;
23import android.content.CursorLoader;
24import android.content.Intent;
25import android.content.IntentFilter;
26import android.content.Loader;
27import android.content.SharedPreferences;
28import android.database.Cursor;
29import android.database.CursorWrapper;
30import android.os.Bundle;
31import android.provider.ContactsContract;
32import android.view.View;
33import android.view.ViewGroup;
34import android.widget.Button;
35import android.widget.CursorAdapter;
36import android.widget.LinearLayout;
37import android.widget.ListView;
38import android.widget.TextView;
39import android.widget.Toast;
40
41/**
42 * Demonstrates selecting contacts that have changed since a certain time.
43 */
44public class ChangedContacts extends Activity implements LoaderManager.LoaderCallbacks<Cursor> {
45
46    private static final String CLASS = ChangedContacts.class.getSimpleName();
47
48    private static final String PREF_KEY_CHANGE = "timestamp_change";
49    private static final String PREF_KEY_DELETE = "timestamp_delete";
50
51    private static final int ID_CHANGE_LOADER = 1;
52    private static final int ID_DELETE_LOADER = 2;
53
54    /**
55     * To see this in action, "clear data" for the contacts storage app in the system settings.
56     * Then come into this app and hit any of the delta buttons.  This will cause the contacts
57     * database to be re-created.
58     */
59    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
60        @Override
61        public void onReceive(Context context, Intent intent) {
62            Toast toast = Toast.makeText(context, "Contacts database created.", Toast.LENGTH_SHORT);
63            toast.show();
64        }
65    };
66
67    private DeleteAdapter mDeleteAdapter;
68    private ChangeAdapter mChangeAdapter;
69    private long mSearchTime;
70    private TextView mDisplayView;
71    private ListView mList;
72    private Button mDeleteButton;
73    private Button mChangeButton;
74
75    @Override
76    protected void onCreate(Bundle savedInstanceState) {
77        super.onCreate(savedInstanceState);
78
79        mDeleteAdapter = new DeleteAdapter(this, null, 0);
80        mChangeAdapter = new ChangeAdapter(this, null, 0);
81
82        LinearLayout main = new LinearLayout(this);
83        main.setOrientation(LinearLayout.VERTICAL);
84
85        mChangeButton = new Button(this);
86        mChangeButton.setText("Changed since " + getLastTimestamp(0, PREF_KEY_CHANGE));
87        mChangeButton.setOnClickListener(new View.OnClickListener() {
88            @Override
89            public void onClick(View v) {
90                changeClick();
91            }
92        });
93
94        mDeleteButton = new Button(this);
95        mDeleteButton.setText("Deleted since " + getLastTimestamp(0, PREF_KEY_DELETE));
96        mDeleteButton.setOnClickListener(new View.OnClickListener() {
97            @Override
98            public void onClick(View v) {
99                deleteClick();
100            }
101        });
102
103        main.addView(mChangeButton);
104        main.addView(mDeleteButton);
105
106        mDisplayView = new TextView(this);
107        mDisplayView.setPadding(5, 5, 5, 5);
108        main.addView(mDisplayView);
109
110        mList = new ListView(this);
111        mList.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
112                ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
113        main.addView(mList);
114
115        setContentView(main);
116    }
117
118    @Override
119    protected void onResume() {
120        super.onResume();
121        IntentFilter filter = new IntentFilter();
122        filter.addAction(ContactsContract.Intents.CONTACTS_DATABASE_CREATED);
123        registerReceiver(mReceiver, filter);
124    }
125
126    @Override
127    protected void onPause() {
128        super.onPause();
129        unregisterReceiver(mReceiver);
130    }
131
132    private void changeClick() {
133        mChangeAdapter.swapCursor(null);
134        LoaderManager manager = getLoaderManager();
135        manager.destroyLoader(ID_DELETE_LOADER);
136        manager.restartLoader(ID_CHANGE_LOADER, null, this);
137    }
138
139    private void deleteClick() {
140        mChangeAdapter.swapCursor(null);
141        LoaderManager manager = getLoaderManager();
142        manager.destroyLoader(ID_CHANGE_LOADER);
143        manager.restartLoader(ID_DELETE_LOADER, null, this);
144    }
145
146    private void saveLastTimestamp(long time, String key) {
147        SharedPreferences pref = getSharedPreferences(CLASS, Context.MODE_PRIVATE);
148        SharedPreferences.Editor editor = pref.edit();
149        editor.putLong(key, time);
150        editor.commit();
151    }
152
153    private long getLastTimestamp(long time, String key) {
154        SharedPreferences pref = getSharedPreferences(CLASS, Context.MODE_PRIVATE);
155        return pref.getLong(key, time);
156    }
157
158    @Override
159    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
160        switch(id) {
161            case ID_CHANGE_LOADER:
162                return getChangeLoader();
163            case ID_DELETE_LOADER:
164                return getDeleteLoader();
165        }
166        return null;
167    }
168
169    private CursorLoader getChangeLoader() {
170        String[] projection = new String[]{
171                ContactsContract.Data._ID,
172                ContactsContract.Data.CONTACT_ID,
173                ContactsContract.Data.DISPLAY_NAME,
174                ContactsContract.Data.CONTACT_LAST_UPDATED_TIMESTAMP
175        };
176
177        mSearchTime = getLastTimestamp(0, PREF_KEY_CHANGE);
178
179        String selection = ContactsContract.Data.CONTACT_LAST_UPDATED_TIMESTAMP + " > ?";
180        String[] bindArgs = new String[]{mSearchTime + ""};
181        return new CursorLoader(this, ContactsContract.Data.CONTENT_URI, projection,
182                selection, bindArgs, ContactsContract.Data.CONTACT_LAST_UPDATED_TIMESTAMP
183                + " desc, " + ContactsContract.Data.CONTACT_ID + " desc");
184    }
185
186    private CursorLoader getDeleteLoader() {
187        String[] projection = new String[]{
188                ContactsContract.DeletedContacts.CONTACT_ID,
189                ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP
190        };
191
192        mSearchTime = getLastTimestamp(0, PREF_KEY_DELETE);
193
194        String selection = ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP + " > ?";
195        String[] bindArgs = new String[]{mSearchTime + ""};
196        return new CursorLoader(this, ContactsContract.DeletedContacts.CONTENT_URI, projection,
197                selection, bindArgs, ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP +
198                " desc");
199    }
200
201    @Override
202    public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor data) {
203        long timestamp = 0;
204
205
206        switch (cursorLoader.getId()) {
207            case ID_CHANGE_LOADER:
208                mDisplayView.setText(data.getCount() + " change(s) since " + mSearchTime);
209                mList.setAdapter(mChangeAdapter);
210                mChangeAdapter.swapCursor(data);
211
212                // Save the largest timestamp returned.  Only need the first one due to the sort
213                // order.
214                if (data.moveToNext()) {
215                    timestamp = data.getLong(data.getColumnIndex(
216                            ContactsContract.Data.CONTACT_LAST_UPDATED_TIMESTAMP));
217                    data.moveToPrevious();
218                }
219                if (timestamp > 0) {
220                    saveLastTimestamp(timestamp, PREF_KEY_CHANGE);
221                    mChangeButton.setText("Changed since " + timestamp);
222                }
223                break;
224            case ID_DELETE_LOADER:
225                mDisplayView.setText(data.getCount() + " delete(s) since " + mSearchTime);
226                mList.setAdapter(mDeleteAdapter);
227                mDeleteAdapter.swapCursor(new DeleteCursorWrapper(data));
228                if (data.moveToNext()) {
229                    timestamp = data.getLong(data.getColumnIndex(
230                            ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP));
231                    data.moveToPrevious();
232                }
233                if (timestamp > 0) {
234                    saveLastTimestamp(timestamp, PREF_KEY_DELETE);
235                    mDeleteButton.setText("Deleted since " + timestamp);
236                }
237                break;
238        }
239    }
240
241    @Override
242    public void onLoaderReset(Loader<Cursor> cursorLoader) {
243        mDisplayView.setText("");
244        switch (cursorLoader.getId()) {
245            case ID_CHANGE_LOADER:
246                mChangeAdapter.swapCursor(null);
247                break;
248            case ID_DELETE_LOADER:
249                mDeleteAdapter.swapCursor(null);
250                break;
251        }
252    }
253
254    private class DeleteCursorWrapper extends CursorWrapper {
255
256        /**
257         * Creates a cursor wrapper.
258         *
259         * @param cursor The underlying cursor to wrap.
260         */
261        public DeleteCursorWrapper(Cursor cursor) {
262            super(cursor);
263        }
264
265        @Override
266        public int getColumnIndexOrThrow(String columnName) {
267            if (columnName.equals("_id")) {
268                return super.getColumnIndex(ContactsContract.DeletedContacts.CONTACT_ID);
269            }
270            return super.getColumnIndex(columnName);
271        }
272    }
273
274    private static class DeleteAdapter extends CursorAdapter {
275
276        private Context mContext;
277
278        public DeleteAdapter(Context context, Cursor c, int flags) {
279            super(context, c, flags);
280            this.mContext = context;
281        }
282
283        @Override
284        public View newView(Context context, Cursor cursor, ViewGroup parent) {
285            LinearLayout item = new LinearLayout(mContext);
286            item.addView(buildText(context));
287            item.addView(buildText(context));
288            return item;
289        }
290
291        @Override
292        public void bindView(View view, Context context, Cursor cursor) {
293            LinearLayout item = (LinearLayout) view;
294            String id = cursor.getString(cursor.getColumnIndex(
295                    ContactsContract.DeletedContacts.CONTACT_ID));
296            String timestamp = cursor.getString(cursor.getColumnIndex(
297                    ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP));
298
299            setText(item.getChildAt(0), id);
300            setText(item.getChildAt(1), timestamp);
301        }
302    }
303
304    private static class ChangeAdapter extends CursorAdapter {
305
306        private Context mContext;
307
308        public ChangeAdapter(Context context, Cursor c, int flags) {
309            super(context, c, flags);
310            mContext = context;
311        }
312
313        @Override
314        public View newView(Context context, Cursor cursor, ViewGroup parent) {
315            LinearLayout item = new LinearLayout(mContext);
316            item.addView(buildText(context));
317            item.addView(buildText(context));
318            item.addView(buildText(context));
319            return item;
320        }
321
322        @Override
323        public void bindView(View view, Context context, Cursor cursor) {
324            LinearLayout item = (LinearLayout) view;
325
326            String id = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.CONTACT_ID));
327            String name = cursor.getString(cursor.getColumnIndex(
328                    ContactsContract.Data.DISPLAY_NAME));
329            String timestamp = cursor.getString(cursor.getColumnIndex(
330                    ContactsContract.Data.CONTACT_LAST_UPDATED_TIMESTAMP));
331
332            setText(item.getChildAt(0), id);
333            setText(item.getChildAt(1), name);
334            setText(item.getChildAt(2), timestamp);
335        }
336    }
337
338    private static void setText(View view, String value) {
339        TextView text = (TextView) view;
340        text.setText(value);
341    }
342
343    private static TextView buildText(Context context) {
344        TextView view = new TextView(context);
345        view.setPadding(3, 3, 3, 3);
346        return view;
347    }
348}
349