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