1/*
2 * Copyright (C) 2011 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.contacts.common.vcard;
18
19import android.app.Activity;
20import android.app.Notification;
21import android.app.NotificationManager;
22import android.app.PendingIntent;
23import android.content.ContentUris;
24import android.content.Context;
25import android.content.Intent;
26import android.net.Uri;
27import android.os.Handler;
28import android.os.Message;
29import android.provider.ContactsContract;
30import android.provider.ContactsContract.RawContacts;
31import android.support.v4.app.NotificationCompat;
32import android.widget.Toast;
33
34import com.android.contacts.common.R;
35import com.android.vcard.VCardEntry;
36
37import java.text.NumberFormat;
38
39public class NotificationImportExportListener implements VCardImportExportListener,
40        Handler.Callback {
41    /** The tag used by vCard-related notifications. */
42    /* package */ static final String DEFAULT_NOTIFICATION_TAG = "VCardServiceProgress";
43    /**
44     * The tag used by vCard-related failure notifications.
45     * <p>
46     * Use a different tag from {@link #DEFAULT_NOTIFICATION_TAG} so that failures do not get
47     * replaced by other notifications and vice-versa.
48     */
49    /* package */ static final String FAILURE_NOTIFICATION_TAG = "VCardServiceFailure";
50
51    private final NotificationManager mNotificationManager;
52    private final Activity mContext;
53    private final Handler mHandler;
54
55    public NotificationImportExportListener(Activity activity) {
56        mContext = activity;
57        mNotificationManager = (NotificationManager) activity.getSystemService(
58                Context.NOTIFICATION_SERVICE);
59        mHandler = new Handler(this);
60    }
61
62    @Override
63    public boolean handleMessage(Message msg) {
64        String text = (String) msg.obj;
65        Toast.makeText(mContext, text, Toast.LENGTH_LONG).show();
66        return true;
67    }
68
69    @Override
70    public void onImportProcessed(ImportRequest request, int jobId, int sequence) {
71        // Show a notification about the status
72        final String displayName;
73        final String message;
74        if (request.displayName != null) {
75            displayName = request.displayName;
76            message = mContext.getString(R.string.vcard_import_will_start_message, displayName);
77        } else {
78            displayName = mContext.getString(R.string.vcard_unknown_filename);
79            message = mContext.getString(
80                    R.string.vcard_import_will_start_message_with_default_name);
81        }
82
83        // We just want to show notification for the first vCard.
84        if (sequence == 0) {
85            // TODO: Ideally we should detect the current status of import/export and
86            // show "started" when we can import right now and show "will start" when
87            // we cannot.
88            mHandler.obtainMessage(0, message).sendToTarget();
89        }
90
91        final Notification notification = constructProgressNotification(mContext,
92                VCardService.TYPE_IMPORT, message, message, jobId, displayName, -1, 0);
93        mNotificationManager.notify(DEFAULT_NOTIFICATION_TAG, jobId, notification);
94    }
95
96    @Override
97    public void onImportParsed(ImportRequest request, int jobId, VCardEntry entry, int currentCount,
98            int totalCount) {
99        if (entry.isIgnorable()) {
100            return;
101        }
102
103        final String totalCountString = String.valueOf(totalCount);
104        final String tickerText =
105                mContext.getString(R.string.progress_notifier_message,
106                        String.valueOf(currentCount),
107                        totalCountString,
108                        entry.getDisplayName());
109        final String description = mContext.getString(R.string.importing_vcard_description,
110                entry.getDisplayName());
111
112        final Notification notification = constructProgressNotification(
113                mContext.getApplicationContext(), VCardService.TYPE_IMPORT, description, tickerText,
114                jobId, request.displayName, totalCount, currentCount);
115        mNotificationManager.notify(DEFAULT_NOTIFICATION_TAG, jobId, notification);
116    }
117
118    @Override
119    public void onImportFinished(ImportRequest request, int jobId, Uri createdUri) {
120        final String description = mContext.getString(R.string.importing_vcard_finished_title,
121                request.displayName);
122        final Intent intent;
123        if (createdUri != null) {
124            final long rawContactId = ContentUris.parseId(createdUri);
125            final Uri contactUri = RawContacts.getContactLookupUri(
126                    mContext.getContentResolver(), ContentUris.withAppendedId(
127                            RawContacts.CONTENT_URI, rawContactId));
128            intent = new Intent(Intent.ACTION_VIEW, contactUri);
129        } else {
130            intent = new Intent(Intent.ACTION_VIEW);
131            intent.setType(ContactsContract.Contacts.CONTENT_TYPE);
132        }
133        intent.setPackage(mContext.getPackageName());
134        final Notification notification =
135                NotificationImportExportListener.constructFinishNotification(mContext,
136                description, null, intent);
137        mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
138                jobId, notification);
139    }
140
141    @Override
142    public void onImportFailed(ImportRequest request) {
143        // TODO: a little unkind to show Toast in this case, which is shown just a moment.
144        // Ideally we should show some persistent something users can notice more easily.
145        mHandler.obtainMessage(0,
146                mContext.getString(R.string.vcard_import_request_rejected_message)).sendToTarget();
147    }
148
149    @Override
150    public void onImportCanceled(ImportRequest request, int jobId) {
151        final String description = mContext.getString(R.string.importing_vcard_canceled_title,
152                request.displayName);
153        final Notification notification =
154                NotificationImportExportListener.constructCancelNotification(mContext, description);
155        mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
156                jobId, notification);
157    }
158
159    @Override
160    public void onExportProcessed(ExportRequest request, int jobId) {
161        final String displayName = ExportVCardActivity.getOpenableUriDisplayName(mContext,
162                request.destUri);
163        final String message = mContext.getString(R.string.contacts_export_will_start_message);
164
165        mHandler.obtainMessage(0, message).sendToTarget();
166        final Notification notification =
167                NotificationImportExportListener.constructProgressNotification(mContext,
168                        VCardService.TYPE_EXPORT, message, message, jobId, displayName, -1, 0);
169        mNotificationManager.notify(DEFAULT_NOTIFICATION_TAG, jobId, notification);
170    }
171
172    @Override
173    public void onExportFailed(ExportRequest request) {
174        mHandler.obtainMessage(0,
175                mContext.getString(R.string.vcard_export_request_rejected_message)).sendToTarget();
176    }
177
178    @Override
179    public void onCancelRequest(CancelRequest request, int type) {
180        final String description = type == VCardService.TYPE_IMPORT ?
181                mContext.getString(R.string.importing_vcard_canceled_title, request.displayName) :
182                mContext.getString(R.string.exporting_vcard_canceled_title, request.displayName);
183        final Notification notification = constructCancelNotification(mContext, description);
184        mNotificationManager.notify(DEFAULT_NOTIFICATION_TAG, request.jobId, notification);
185    }
186
187    /**
188     * Constructs a {@link Notification} showing the current status of import/export.
189     * Users can cancel the process with the Notification.
190     *
191     * @param context
192     * @param type import/export
193     * @param description Content of the Notification.
194     * @param tickerText
195     * @param jobId
196     * @param displayName Name to be shown to the Notification (e.g. "finished importing XXXX").
197     * Typycally a file name.
198     * @param totalCount The number of vCard entries to be imported. Used to show progress bar.
199     * -1 lets the system show the progress bar with "indeterminate" state.
200     * @param currentCount The index of current vCard. Used to show progress bar.
201     */
202    /* package */ static Notification constructProgressNotification(
203            Context context, int type, String description, String tickerText,
204            int jobId, String displayName, int totalCount, int currentCount) {
205        // Note: We cannot use extra values here (like setIntExtra()), as PendingIntent doesn't
206        // preserve them across multiple Notifications. PendingIntent preserves the first extras
207        // (when flag is not set), or update them when PendingIntent#getActivity() is called
208        // (See PendingIntent#FLAG_UPDATE_CURRENT). In either case, we cannot preserve extras as we
209        // expect (for each vCard import/export request).
210        //
211        // We use query parameter in Uri instead.
212        // Scheme and Authority is arbitorary, assuming CancelActivity never refers them.
213        final Intent intent = new Intent(context, CancelActivity.class);
214        final Uri uri = (new Uri.Builder())
215                .scheme("invalidscheme")
216                .authority("invalidauthority")
217                .appendQueryParameter(CancelActivity.JOB_ID, String.valueOf(jobId))
218                .appendQueryParameter(CancelActivity.DISPLAY_NAME, displayName)
219                .appendQueryParameter(CancelActivity.TYPE, String.valueOf(type)).build();
220        intent.setData(uri);
221
222        final NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
223        builder.setOngoing(true)
224                .setProgress(totalCount, currentCount, totalCount == - 1)
225                .setTicker(tickerText)
226                .setContentTitle(description)
227                .setColor(context.getResources().getColor(R.color.dialtacts_theme_color))
228                .setSmallIcon(type == VCardService.TYPE_IMPORT
229                        ? android.R.drawable.stat_sys_download
230                        : android.R.drawable.stat_sys_upload)
231                .setContentIntent(PendingIntent.getActivity(context, 0, intent, 0));
232        if (totalCount > 0) {
233            String percentage =
234                    NumberFormat.getPercentInstance().format((double) currentCount / totalCount);
235            builder.setContentText(percentage);
236        }
237        return builder.getNotification();
238    }
239
240    /**
241     * Constructs a Notification telling users the process is canceled.
242     *
243     * @param context
244     * @param description Content of the Notification
245     */
246    /* package */ static Notification constructCancelNotification(
247            Context context, String description) {
248        return new NotificationCompat.Builder(context)
249                .setAutoCancel(true)
250                .setSmallIcon(android.R.drawable.stat_notify_error)
251                .setColor(context.getResources().getColor(R.color.dialtacts_theme_color))
252                .setContentTitle(description)
253                .setContentText(description)
254                // Launch an intent that won't resolve to anything. Restrict the intent to this
255                // app to make sure that no other app can steal this pending-intent b/19296918.
256                .setContentIntent(PendingIntent
257                        .getActivity(context, 0, new Intent(context.getPackageName(), null), 0))
258                .getNotification();
259    }
260
261    /**
262     * Constructs a Notification telling users the process is finished.
263     *
264     * @param context
265     * @param description Content of the Notification
266     * @param intent Intent to be launched when the Notification is clicked. Can be null.
267     */
268    /* package */ static Notification constructFinishNotification(
269            Context context, String title, String description, Intent intent) {
270        return constructFinishNotificationWithFlags(context, title, description, intent, 0);
271    }
272
273    /**
274     * @param flags use FLAG_ACTIVITY_NEW_TASK to set it as new task, to get rid of cached files.
275     */
276    /* package */ static Notification constructFinishNotificationWithFlags(
277            Context context, String title, String description, Intent intent, int flags) {
278        return new NotificationCompat.Builder(context)
279                .setAutoCancel(true)
280                .setColor(context.getResources().getColor(R.color.dialtacts_theme_color))
281                .setSmallIcon(android.R.drawable.stat_sys_download_done)
282                .setContentTitle(title)
283                .setContentText(description)
284                // If no intent provided, include an intent that won't resolve to anything.
285                // Restrict the intent to this app to make sure that no other app can steal this
286                // pending-intent b/19296918.
287                .setContentIntent(PendingIntent.getActivity(context, 0,
288                        (intent != null ? intent : new Intent(context.getPackageName(), null)),
289                        flags))
290                .getNotification();
291    }
292
293    /**
294     * Constructs a Notification telling the vCard import has failed.
295     *
296     * @param context
297     * @param reason The reason why the import has failed. Shown in description field.
298     */
299    /* package */ static Notification constructImportFailureNotification(
300            Context context, String reason) {
301        return new NotificationCompat.Builder(context)
302                .setAutoCancel(true)
303                .setColor(context.getResources().getColor(R.color.dialtacts_theme_color))
304                .setSmallIcon(android.R.drawable.stat_notify_error)
305                .setContentTitle(context.getString(R.string.vcard_import_failed))
306                .setContentText(reason)
307                // Launch an intent that won't resolve to anything. Restrict the intent to this
308                // app to make sure that no other app can steal this pending-intent b/19296918.
309                .setContentIntent(PendingIntent
310                        .getActivity(context, 0, new Intent(context.getPackageName(), null), 0))
311                .getNotification();
312    }
313
314    @Override
315    public void onComplete() {
316        mContext.finish();
317    }
318}
319