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 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 androidx.core.app.NotificationCompat; 32 import android.widget.Toast; 33 34 import com.android.contacts.R; 35 import com.android.contacts.util.ContactsNotificationChannelsUtil; 36 import com.android.vcard.VCardEntry; 37 38 import java.text.NumberFormat; 39 40 public 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 NotificationImportExportListener(Activity activity)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 handleMessage(Message msg)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 onImportProcessed(ImportRequest request, int jobId, int sequence)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 onImportParsed(ImportRequest request, int jobId, VCardEntry entry, int currentCount, int totalCount)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 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 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 onExportFailed(ExportRequest request)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 onCancelRequest(CancelRequest request, int type)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 */ constructProgressNotification( Context context, int type, String description, String tickerText, int jobId, String displayName, int totalCount, int currentCount)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 */ constructCancelNotification( Context context, String description)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 */ constructFinishNotification( Context context, String title, String description, Intent intent)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 */ constructFinishNotificationWithFlags( Context context, String title, String description, Intent intent, int flags)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 */ constructImportFailureNotification( Context context, String reason)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