1 /*
2  * Copyright (C) 2015 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 package android.support.car.ui;
17 
18 import android.app.Notification;
19 import android.content.Intent;
20 import android.graphics.Bitmap;
21 import android.os.Bundle;
22 import android.support.annotation.DrawableRes;
23 import android.support.annotation.IntDef;
24 import android.support.annotation.NonNull;
25 import android.support.annotation.Nullable;
26 import android.support.v4.app.NotificationCompat;
27 
28 import java.lang.annotation.Retention;
29 import java.lang.annotation.RetentionPolicy;
30 
31 /**
32  * Helper class to add navigation extensions to notifications for use in Android Auto.
33  * <p>
34  * To create a notification with navigation extensions:
35  * <ol>
36  *   <li>Create a {@link android.app.Notification.Builder}, setting any desired
37  *   properties.
38  *   <li>Create a {@link CarNavExtender}.
39  *   <li>Set car-specific properties using the
40  *   {@code add} and {@code set} methods of {@link CarNavExtender}.
41  *   <li>Call {@link android.app.Notification.Builder#extend} to apply the extensions to a
42  *   notification.
43  *   <li>Post the notification to the notification system with the
44  *   {@code NotificationManager.notify(...)} methods.
45  * </ol>
46  *
47  * <pre class="prettyprint">
48  * Notification notif = new Notification.Builder(mContext)
49  *         .setContentTitle("Turn right in 2.0 miles on to US 101-N")
50  *         .setContentText("43 mins (32 mi) to Home")
51  *         .setSmallIcon(R.drawable.ic_nav)
52  *         .extend(new CarNavExtender()
53  *                 .setContentTitle("US 101-N")
54  *                 .setContentText("400 ft")
55  *                 .setSubText("43 mins to Home")
56  *         .build();
57  * NotificationManager notificationManger =
58  *         (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
59  * notificationManger.notify(0, notif);</pre>
60  *
61  * <p>CarNavExtender fields can be accessed on an existing notification by using the
62  * {@code CarNavExtender(Notification)} constructor,
63  * and then using the {@code get} methods to access values.
64  * @hide
65  */
66 public class CarNavExtender implements NotificationCompat.Extender {
67     /** This value must remain unchanged for compatibility. **/
68     private static final String EXTRA_CAR_EXTENDER = "android.car.EXTENSIONS";
69     private static final String EXTRA_IS_EXTENDED =
70             "com.google.android.gms.car.support.CarNavExtender.EXTENDED";
71     private static final String EXTRA_CONTENT_ID = "content_id";
72     private static final String EXTRA_TYPE = "type";
73     private static final String EXTRA_SUB_TEXT = "sub_text";
74     private static final String EXTRA_ACTION_ICON = "action_icon";
75     /** This value must remain unchanged for compatibility. **/
76     private static final String EXTRA_CONTENT_INTENT = "content_intent";
77     /** This value must remain unchanged for compatibility. **/
78     private static final String EXTRA_COLOR = "app_color";
79     private static final String EXTRA_NIGHT_COLOR = "app_night_color";
80     /** This value must remain unchanged for compatibility. **/
81     private static final String EXTRA_STREAM_VISIBILITY = "stream_visibility";
82     /** This value must remain unchanged for compatibility. **/
83     private static final String EXTRA_HEADS_UP_VISIBILITY = "heads_up_visibility";
84     private static final String EXTRA_IGNORE_IN_STREAM = "ignore_in_stream";
85 
86     @IntDef({TYPE_HERO, TYPE_NORMAL})
87     @Retention(RetentionPolicy.SOURCE)
88     private @interface Type {}
89     public static final int TYPE_HERO = 0;
90     public static final int TYPE_NORMAL = 1;
91 
92     private boolean mIsExtended;
93     /** <code>null</code> if not explicitly set. **/
94     private Long mContentId;
95     private int mType = TYPE_NORMAL;
96     private CharSequence mContentTitle;
97     private CharSequence mContentText;
98     private CharSequence mSubText;
99     private Bitmap mLargeIcon;
100     private @DrawableRes int mActionIcon;
101     private Intent mContentIntent;
102     private int mColor = Notification.COLOR_DEFAULT;
103     private int mNightColor = Notification.COLOR_DEFAULT;
104     private boolean mShowInStream = true;
105     private boolean mShowAsHeadsUp;
106     private boolean mIgnoreInStream;
107 
108     /**
109      * Create a new CarNavExtender to extend a new notification.
110      */
CarNavExtender()111     public CarNavExtender() {
112     }
113 
114     /**
115      * Reconstruct a CarNavExtender from an existing notification. Can be used to retrieve values.
116      *
117      * @param notification The notification to retrieve the values from.
118      */
CarNavExtender(@onNull Notification notification)119     public CarNavExtender(@NonNull Notification notification) {
120         Bundle extras = NotificationCompat.getExtras(notification);
121         if (extras == null) {
122             return;
123         }
124         Bundle b = extras.getBundle(EXTRA_CAR_EXTENDER);
125         if (b == null) {
126             return;
127         }
128 
129         mIsExtended = b.getBoolean(EXTRA_IS_EXTENDED);
130         mContentId = (Long) b.getSerializable(EXTRA_CONTENT_ID);
131         // The ternary guarantees that we return either TYPE_HERO or TYPE_NORMAL.
132         mType = (b.getInt(EXTRA_TYPE, TYPE_NORMAL) == TYPE_HERO) ? TYPE_HERO : TYPE_NORMAL;
133         mContentTitle = b.getCharSequence(Notification.EXTRA_TITLE);
134         mContentText = b.getCharSequence(Notification.EXTRA_TEXT);
135         mSubText = b.getCharSequence(EXTRA_SUB_TEXT);
136         mLargeIcon = b.getParcelable(Notification.EXTRA_LARGE_ICON);
137         mActionIcon = b.getInt(EXTRA_ACTION_ICON);
138         mContentIntent = b.getParcelable(EXTRA_CONTENT_INTENT);
139         mColor = b.getInt(EXTRA_COLOR, Notification.COLOR_DEFAULT);
140         mNightColor = b.getInt(EXTRA_NIGHT_COLOR, Notification.COLOR_DEFAULT);
141         mShowInStream = b.getBoolean(EXTRA_STREAM_VISIBILITY, true);
142         mShowAsHeadsUp = b.getBoolean(EXTRA_HEADS_UP_VISIBILITY);
143         mIgnoreInStream = b.getBoolean(EXTRA_IGNORE_IN_STREAM);
144     }
145 
146     @Override
extend(NotificationCompat.Builder builder)147     public NotificationCompat.Builder extend(NotificationCompat.Builder builder) {
148         Bundle b = new Bundle();
149         b.putBoolean(EXTRA_IS_EXTENDED, true);
150         b.putSerializable(EXTRA_CONTENT_ID, mContentId);
151         b.putInt(EXTRA_TYPE, mType);
152         b.putCharSequence(Notification.EXTRA_TITLE, mContentTitle);
153         b.putCharSequence(Notification.EXTRA_TEXT, mContentText);
154         b.putCharSequence(EXTRA_SUB_TEXT, mSubText);
155         b.putParcelable(Notification.EXTRA_LARGE_ICON, mLargeIcon);
156         b.putInt(EXTRA_ACTION_ICON, mActionIcon);
157         b.putParcelable(EXTRA_CONTENT_INTENT, mContentIntent);
158         b.putInt(EXTRA_COLOR, mColor);
159         b.putInt(EXTRA_NIGHT_COLOR, mNightColor);
160         b.putBoolean(EXTRA_STREAM_VISIBILITY, mShowInStream);
161         b.putBoolean(EXTRA_HEADS_UP_VISIBILITY, mShowAsHeadsUp);
162         b.putBoolean(EXTRA_IGNORE_IN_STREAM, mIgnoreInStream);
163         builder.getExtras().putBundle(EXTRA_CAR_EXTENDER, b);
164         return builder;
165     }
166 
167     /**
168      * @return <code>true</code> if the notification was extended with {@link CarNavExtender}.
169      */
isExtended()170     public boolean isExtended() {
171         return mIsExtended;
172     }
173 
174     /**
175      * Static version of {@link #isExtended()}.
176      */
isExtended(Notification notification)177     public static boolean isExtended(Notification notification) {
178         Bundle extras = NotificationCompat.getExtras(notification);
179         if (extras == null) {
180             return false;
181         }
182 
183         extras = extras.getBundle(EXTRA_CAR_EXTENDER);
184         return extras != null && extras.getBoolean(EXTRA_IS_EXTENDED);
185     }
186 
187     /**
188      * Sets an id for the content of this notification. If the content id matches an existing
189      * notification, any timers that control ranking and heads up notification will remain
190      * unchanged. However, if it differs from the previous notification with the same id then
191      * this notification will be treated as a new notification with respect to heads up
192      * notifications and ranking.
193      *
194      * If no content id is specified, it will be treated like a new content id.
195      *
196      * A content id will only be compared to the existing notification, not the entire history of
197      * content ids.
198      *
199      * @param contentId The content id that represents this notification.
200      * @return This object for method chaining.
201      */
setContentId(long contentId)202     public CarNavExtender setContentId(long contentId) {
203         mContentId = contentId;
204         return this;
205     }
206 
207     /**
208      * @return The content id for this notification or <code>null</code> if it was not specified.
209      */
210     @Nullable
getContentId()211     public Long getContentId() {
212         return mContentId;
213     }
214 
215     /**
216      * @param type The type of notification that this will be displayed as in the Android Auto.
217      * @return This object for method chaining.
218      *
219      * @see #TYPE_NORMAL
220      * @see #TYPE_HERO
221      */
setType(@ype int type)222     public CarNavExtender setType(@Type int type) {
223         mType = type;
224         return this;
225     }
226 
227     /**
228      * @return The type of notification
229      *
230      * @see #TYPE_NORMAL
231      * @see #TYPE_HERO
232      */
233     @Type
getType()234     public int getType() {
235         return mType;
236     }
237 
238     /**
239      * @return The type without having to construct an entire {@link CarNavExtender} object.
240      */
241     @Type
getType(Notification notification)242     public static int getType(Notification notification) {
243         Bundle extras = NotificationCompat.getExtras(notification);
244         if (extras == null) {
245             return TYPE_NORMAL;
246         }
247         Bundle b = extras.getBundle(EXTRA_CAR_EXTENDER);
248         if (b == null) {
249             return TYPE_NORMAL;
250         }
251 
252         // The ternary guarantees that we return either TYPE_HERO or TYPE_NORMAL.
253         return (b.getInt(EXTRA_TYPE, TYPE_NORMAL) == TYPE_HERO) ? TYPE_HERO : TYPE_NORMAL;
254     }
255 
256     /**
257      * @param contentTitle Override for the notification's content title.
258      * @return This object for method chaining.
259      */
setContentTitle(CharSequence contentTitle)260     public CarNavExtender setContentTitle(CharSequence contentTitle) {
261         mContentTitle = contentTitle;
262         return this;
263     }
264 
265     /**
266      * @return The content title for the notification if one was explicitly set with
267      *         {@link #setContentTitle(CharSequence)}.
268      */
getContentTitle()269     public CharSequence getContentTitle() {
270         return mContentTitle;
271     }
272 
273     /**
274      * @param contentText Override for the notification's content text. If set to an empty string,
275      *                    it will be treated as if there is no context text by the UI.
276      * @return This object for method chaining.
277      */
setContentText(CharSequence contentText)278     public CarNavExtender setContentText(CharSequence contentText) {
279         mContentText = contentText;
280         return this;
281     }
282 
283     /**
284      * @return The content text for the notification if one was explicitly set with
285      *         {@link #setContentText(CharSequence)}.
286      */
287     @Nullable
getContentText()288     public CharSequence getContentText() {
289         return mContentText;
290     }
291 
292     /**
293      * @param subText A third text field that will be displayed on hero cards.
294      * @return This object for method chaining.
295      */
setSubText(CharSequence subText)296     public CarNavExtender setSubText(CharSequence subText) {
297         mSubText = subText;
298         return this;
299     }
300 
301     /**
302      * @return The secondary content text for the notification or null if it wasn't set.
303      */
304     @Nullable
getSubText()305     public CharSequence getSubText() {
306         return mSubText;
307     }
308 
309     /**
310      * @param largeIcon Override for the notification's large icon.
311      * @return This object for method chaining.
312      */
setLargeIcon(Bitmap largeIcon)313     public CarNavExtender setLargeIcon(Bitmap largeIcon) {
314         mLargeIcon = largeIcon;
315         return this;
316     }
317 
318     /**
319      * @return The large icon for the notification if one was explicitly set with
320      *         {@link #setLargeIcon(android.graphics.Bitmap)}.
321      */
getLargeIcon()322     public Bitmap getLargeIcon() {
323         return mLargeIcon;
324     }
325 
326     /**
327      * By default, Android Auto will show a navigation chevron on cards. However, a separate icon
328      * can be set here to override it.
329      *
330      * @param actionIcon The action icon resource id from your package that you would like to
331      *                   use instead of the navigation chevron.
332      * @return This object for method chaining.
333      */
setActionIcon(@rawableRes int actionIcon)334     public CarNavExtender setActionIcon(@DrawableRes int actionIcon) {
335         mActionIcon = actionIcon;
336         return this;
337     }
338 
339     /**
340      * @return The overridden action icon or 0 if one wasn't set.
341      */
342     @DrawableRes
getActionIcon()343     public int getActionIcon() {
344         return mActionIcon;
345     }
346 
347     /**
348      * @param contentIntent The content intent that will be sent using
349      *                      {@link com.google.android.gms.car.CarActivity#startCarProjectionActivity(android.content.Intent)}
350      *                      It is STRONGLY suggested that you set a content intent or else the
351      *                      notification will have no action when tapped.
352      * @return This object for method chaining.
353      */
setContentIntent(Intent contentIntent)354     public CarNavExtender setContentIntent(Intent contentIntent) {
355         mContentIntent = contentIntent;
356         return this;
357     }
358 
359     /**
360      * @return The content intent that will be sent using
361      *         {@link com.google.android.gms.car.CarActivity#startCarProjectionActivity(android.content.Intent)}
362      */
getContentIntent()363     public Intent getContentIntent() {
364         return mContentIntent;
365     }
366 
367     /**
368      * @param color Override for the notification color.
369      * @return This object for method chaining.
370      *
371      * @see android.app.Notification.Builder#setColor(int)
372      */
setColor(int color)373     public CarNavExtender setColor(int color) {
374         mColor = color;
375         return this;
376     }
377 
378     /**
379      * @return The color specified by the notification or {@link android.app.Notification#COLOR_DEFAULT} if
380      *         one wasn't explicitly set with {@link #setColor(int)}.
381      */
getColor()382     public int getColor() {
383         return mColor;
384     }
385 
386     /**
387      * @param nightColor Override for the notification color at night.
388      * @return This object for method chaining.
389      *
390      * @see android.app.Notification.Builder#setColor(int)
391      */
setNightColor(int nightColor)392     public CarNavExtender setNightColor(int nightColor) {
393         mNightColor = nightColor;
394         return this;
395     }
396 
397     /**
398      * @return The night color specified by the notification or {@link android.app.Notification#COLOR_DEFAULT}
399      *         if one wasn't explicitly set with {@link #setNightColor(int)}.
400      */
getNightColor()401     public int getNightColor() {
402         return mNightColor;
403     }
404 
405     /**
406      * @param show Whether or not to show the notification in the stream.
407      * @return This object for method chaining.
408      */
setShowInStream(boolean show)409     public CarNavExtender setShowInStream(boolean show) {
410         mShowInStream = show;
411         return this;
412     }
413 
414     /**
415      * @return Whether or not to show the notification in the stream.
416      */
getShowInStream()417     public boolean getShowInStream() {
418         return mShowInStream;
419     }
420 
421     /**
422      * @param show Whether or not to show the notification as a heads up notification.
423      * @return This object for method chaining.
424      */
setShowAsHeadsUp(boolean show)425     public CarNavExtender setShowAsHeadsUp(boolean show) {
426         mShowAsHeadsUp = show;
427         return this;
428     }
429 
430     /**
431      * @return Whether or not to show the notification as a heads up notification.
432      */
getShowAsHeadsUp()433     public boolean getShowAsHeadsUp() {
434         return mShowAsHeadsUp;
435     }
436 
437     /**
438      * @param ignore Whether or not this notification can be shown as a heads-up notification if
439      *               the user is already on the stream.
440      * @return This object for method chaining.
441      */
setIgnoreInStream(boolean ignore)442     public CarNavExtender setIgnoreInStream(boolean ignore) {
443         mIgnoreInStream = ignore;
444         return this;
445     }
446 
447     /**
448      * @return Whether or not the stream item can be shown as a heads-up notification if ther user
449      *         already is on the stream.
450      */
getIgnoreInStream()451     public boolean getIgnoreInStream() {
452         return mIgnoreInStream;
453     }
454 }