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