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 
17 package com.android.contacts.common.vcard;
18 
19 import android.app.Activity;
20 import android.app.Notification;
21 import android.app.NotificationManager;
22 import android.app.PendingIntent;
23 import android.content.ContentUris;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.net.Uri;
27 import android.os.Handler;
28 import android.os.Message;
29 import android.provider.ContactsContract;
30 import android.provider.ContactsContract.RawContacts;
31 import android.support.v4.app.NotificationCompat;
32 import android.widget.Toast;
33 
34 import com.android.contacts.common.R;
35 import com.android.vcard.VCardEntry;
36 
37 import java.text.NumberFormat;
38 
39 public 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 
NotificationImportExportListener(Activity activity)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
handleMessage(Message msg)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
onImportProcessed(ImportRequest request, int jobId, int sequence)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
onImportParsed(ImportRequest request, int jobId, VCardEntry entry, int currentCount, int totalCount)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
onImportFinished(ImportRequest request, int jobId, Uri createdUri)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
onImportFailed(ImportRequest request)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
onImportCanceled(ImportRequest request, int jobId)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
onExportProcessed(ExportRequest request, int jobId)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
onExportFailed(ExportRequest request)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
onCancelRequest(CancelRequest request, int type)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      */
constructProgressNotification( Context context, int type, String description, String tickerText, int jobId, String displayName, int totalCount, int currentCount)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      */
constructCancelNotification( Context context, String description)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      */
constructFinishNotification( Context context, String title, String description, Intent intent)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      */
constructFinishNotificationWithFlags( Context context, String title, String description, Intent intent, int flags)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      */
constructImportFailureNotification( Context context, String reason)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
onComplete()315     public void onComplete() {
316         mContext.finish();
317     }
318 }
319