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