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