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