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