• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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