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 return constructProgressNotification(mContext.getApplicationContext(), 116 VCardService.TYPE_IMPORT, description, tickerText, jobId, request.displayName, 117 totalCount, currentCount); 118 } 119 120 @Override onImportFinished(ImportRequest request, int jobId, Uri createdUri)121 public void onImportFinished(ImportRequest request, int jobId, Uri createdUri) { 122 final String description = mContext.getString(R.string.importing_vcard_finished_title, 123 request.displayName); 124 final Intent intent; 125 if (createdUri != null) { 126 final long rawContactId = ContentUris.parseId(createdUri); 127 final Uri contactUri = RawContacts.getContactLookupUri( 128 mContext.getContentResolver(), ContentUris.withAppendedId( 129 RawContacts.CONTENT_URI, rawContactId)); 130 intent = new Intent(Intent.ACTION_VIEW, contactUri); 131 } else { 132 intent = new Intent(Intent.ACTION_VIEW); 133 intent.setType(ContactsContract.Contacts.CONTENT_TYPE); 134 } 135 intent.setPackage(mContext.getPackageName()); 136 final Notification notification = 137 NotificationImportExportListener.constructFinishNotification(mContext, 138 description, null, intent); 139 mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG, 140 jobId, notification); 141 } 142 143 @Override onImportFailed(ImportRequest request)144 public void onImportFailed(ImportRequest request) { 145 // TODO: a little unkind to show Toast in this case, which is shown just a moment. 146 // Ideally we should show some persistent something users can notice more easily. 147 mHandler.obtainMessage(0, 148 mContext.getString(R.string.vcard_import_request_rejected_message)).sendToTarget(); 149 } 150 151 @Override onImportCanceled(ImportRequest request, int jobId)152 public void onImportCanceled(ImportRequest request, int jobId) { 153 final String description = mContext.getString(R.string.importing_vcard_canceled_title, 154 request.displayName); 155 final Notification notification = 156 NotificationImportExportListener.constructCancelNotification(mContext, description); 157 mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG, 158 jobId, notification); 159 } 160 161 @Override onExportProcessed(ExportRequest request, int jobId)162 public Notification onExportProcessed(ExportRequest request, int jobId) { 163 final String displayName = request.displayName; 164 final String message = mContext.getString(R.string.contacts_export_will_start_message); 165 166 mHandler.obtainMessage(0, message).sendToTarget(); 167 ContactsNotificationChannelsUtil.createDefaultChannel(mContext); 168 return constructProgressNotification(mContext, VCardService.TYPE_EXPORT, message, message, 169 jobId, displayName, -1, 0); 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 .setChannelId(ContactsNotificationChannelsUtil.DEFAULT_CHANNEL) 225 .setOnlyAlertOnce(true) 226 .setProgress(totalCount, currentCount, totalCount == - 1) 227 .setTicker(tickerText) 228 .setContentTitle(description) 229 .setColor(context.getResources().getColor(R.color.dialtacts_theme_color)) 230 .setSmallIcon(type == VCardService.TYPE_IMPORT 231 ? android.R.drawable.stat_sys_download 232 : android.R.drawable.stat_sys_upload) 233 .setContentIntent(PendingIntent.getActivity(context, 0, intent, FLAG_IMMUTABLE)); 234 if (totalCount > 0) { 235 String percentage = 236 NumberFormat.getPercentInstance().format((double) currentCount / totalCount); 237 builder.setContentText(percentage); 238 } 239 return builder.build(); 240 } 241 242 /** 243 * Constructs a Notification telling users the process is canceled. 244 * 245 * @param context 246 * @param description Content of the Notification 247 */ constructCancelNotification( Context context, String description)248 /* package */ static Notification constructCancelNotification( 249 Context context, String description) { 250 ContactsNotificationChannelsUtil.createDefaultChannel(context); 251 return new NotificationCompat.Builder(context, 252 ContactsNotificationChannelsUtil.DEFAULT_CHANNEL) 253 .setAutoCancel(true) 254 .setSmallIcon(android.R.drawable.stat_notify_error) 255 .setColor(context.getResources().getColor(R.color.dialtacts_theme_color)) 256 .setContentTitle(description) 257 .setContentText(description) 258 .build(); 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 ContactsNotificationChannelsUtil.createDefaultChannel(context); 271 return new NotificationCompat.Builder(context, 272 ContactsNotificationChannelsUtil.DEFAULT_CHANNEL) 273 .setAutoCancel(true) 274 .setColor(context.getResources().getColor(R.color.dialtacts_theme_color)) 275 .setSmallIcon(R.drawable.quantum_ic_done_vd_theme_24) 276 .setContentTitle(title) 277 .setContentText(description) 278 .setContentIntent(PendingIntent.getActivity(context, 0, intent, FLAG_IMMUTABLE)) 279 .build(); 280 } 281 282 /** 283 * Constructs a Notification telling the vCard import has failed. 284 * 285 * @param context 286 * @param reason The reason why the import has failed. Shown in description field. 287 */ constructImportFailureNotification( Context context, String reason)288 /* package */ static Notification constructImportFailureNotification( 289 Context context, String reason) { 290 ContactsNotificationChannelsUtil.createDefaultChannel(context); 291 return new NotificationCompat.Builder(context, 292 ContactsNotificationChannelsUtil.DEFAULT_CHANNEL) 293 .setAutoCancel(true) 294 .setColor(context.getResources().getColor(R.color.dialtacts_theme_color)) 295 .setSmallIcon(android.R.drawable.stat_notify_error) 296 .setContentTitle(context.getString(R.string.vcard_import_failed)) 297 .setContentText(reason) 298 .build(); 299 } 300 } 301