diff options
| author | Xin Li <delphij@google.com> | 2019-09-04 13:34:22 -0700 |
|---|---|---|
| committer | Xin Li <delphij@google.com> | 2019-09-04 13:34:22 -0700 |
| commit | 0a6e8ec04f60904b8e19ec6c218854cebe446e37 (patch) | |
| tree | 86e2e148c268978247a0b10f28ea8e699d7e2fd5 | |
| parent | 1e52ffd9383bbd005976083d9785062597a5aa82 (diff) | |
| parent | c4e4f3761eaf8a02b55eae544701f9a7c6aa165a (diff) | |
| download | platform_packages_apps_Car_Launcher-ndk-sysroot-r21.tar.gz platform_packages_apps_Car_Launcher-ndk-sysroot-r21.tar.bz2 platform_packages_apps_Car_Launcher-ndk-sysroot-r21.zip | |
DO NOT MERGE - Merge Android 10 into masterndk-sysroot-r21
Bug: 139893257
Change-Id: I73df6396cf6dcfbb954c2bddfef186dceb26ea30
34 files changed, 1467 insertions, 360 deletions
diff --git a/Android.bp b/Android.bp deleted file mode 100644 index 2e24262..0000000 --- a/Android.bp +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (C) 2018 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -android_app { - name: "CarLauncher", - srcs: ["src/**/*.java"], - resource_dirs: ["res"], - platform_apis: true, - certificate: "platform", - privileged: true, - overrides: [ - "Launcher2", - "Launcher3", - "Launcher3QuickStep", - ], - optimize: { - enabled: false, - }, - dex_preopt: { - enabled: false, - }, - static_libs: [ - "androidx.car_car", - "androidx-constraintlayout_constraintlayout", - "androidx-constraintlayout_constraintlayout-solver", - ], - libs: ["android.car"], - product_variables: { - pdk: { - enabled: false, - }, - }, -} diff --git a/Android.mk b/Android.mk new file mode 100644 index 0000000..28fd418 --- /dev/null +++ b/Android.mk @@ -0,0 +1,63 @@ +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +ifneq ($(TARGET_BUILD_PDK), true) + +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res + +LOCAL_PACKAGE_NAME := CarLauncher + +LOCAL_PRIVATE_PLATFORM_APIS := true + +LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.car.carlauncher + +LOCAL_CERTIFICATE := platform + +LOCAL_MODULE_TAGS := optional + +LOCAL_PRIVILEGED_MODULE := true + +LOCAL_OVERRIDES_PACKAGES += Launcher2 Launcher3 Launcher3QuickStep + +LOCAL_USE_AAPT2 := true + +LOCAL_PROGUARD_ENABLED := disabled + +LOCAL_DEX_PREOPT := false + +LOCAL_STATIC_JAVA_LIBRARIES := \ + androidx-constraintlayout_constraintlayout-solver + +LOCAL_JAVA_LIBRARIES += android.car + +LOCAL_STATIC_ANDROID_LIBRARIES += \ + androidx-constraintlayout_constraintlayout \ + androidx.lifecycle_lifecycle-extensions \ + car-media-common + +# Including the resources for the static android libraries allows to pick up their static overlays. +LOCAL_RESOURCE_DIR += \ + $(LOCAL_PATH)/../libs/car-apps-common/res \ + $(LOCAL_PATH)/../libs/car-media-common/res + +include $(BUILD_PACKAGE) + +endif diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 8db647e..6b3044a 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -16,46 +16,53 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.car.carlauncher"> - <uses-sdk - android:minSdkVersion="27" - android:targetSdkVersion='27'/> + package="com.android.car.carlauncher" + android:sharedUserId="android.uid.system"> <!-- System permission to get app usage data --> - <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" /> + <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/> + <!-- System permission to send events to hosted maps activity --> + <uses-permission android:name="android.permission.INJECT_EVENTS"/> + <!-- System permissions to bring hosted maps activity to front on main display --> + <uses-permission android:name="android.permission.MANAGE_ACTIVITY_STACKS"/> + <uses-permission android:name="android.permission.REORDER_TASKS"/> + <!-- System permission to host maps activity --> + <uses-permission android:name="android.permission.ACTIVITY_EMBEDDING"/> + <!-- System permission to control media playback of the active session --> + <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/> + <!-- System permission to query users on device --> + <uses-permission android:name="android.permission.MANAGE_USERS"/> + <uses-permission android:name="android.car.permission.ACCESS_CAR_PROJECTION_STATUS"/> <application - android:label="@string/app_title" android:icon="@drawable/ic_launcher_home" - android:theme="@style/Theme.Car.Light.NoActionBar"> - <activity - android:name=".HomeActivity" - android:launchMode="singleInstance" - android:label="@string/app_title" - android:resizeableActivity="true"> - </activity> + android:label="@string/app_title" + android:theme="@style/Theme.Launcher"> <activity android:name=".CarLauncher" - android:theme="@android:style/Theme.NoTitleBar"> + android:configChanges="uiMode|mcc|mnc" + android:launchMode="singleTask" + android:clearTaskOnLaunch="true" + android:stateNotNeeded="true" + android:resumeWhilePausing="true" + android:windowSoftInputMode="adjustPan" + android:screenOrientation="nosensor"> + <meta-data android:name="distractionOptimized" android:value="true"/> <intent-filter> - <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.HOME" /> - <category android:name="android.intent.category.DEFAULT" /> - <category android:name="android.intent.category.LAUNCHER" /> - <category android:name="android.intent.category.LAUNCHER_APP" /> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.HOME"/> + <category android:name="android.intent.category.DEFAULT"/> </intent-filter> </activity> <activity - android:name=".AppGridActivity"> + android:name=".AppGridActivity" + android:launchMode="singleInstance" + android:noHistory="true"> + <meta-data android:name="distractionOptimized" android:value="true"/> </activity> <activity - android:name=".AppSearchActivity"> + android:name=".AppSearchActivity" + android:launchMode="singleInstance" + android:noHistory="true"> + <meta-data android:name="distractionOptimized" android:value="true"/> </activity> - <receiver android:name=".AppGridActivity$AppInstallUninstallReceiver"> - <intent-filter> - <action android:name="android.intent.action.PACKAGE_ADDED"/> - <action android:name="android.intent.action.PACKAGE_CHANGED"/> - <action android:name="android.intent.action.PACKAGE_REMOVED"/> - <data android:scheme="package"/> - </intent-filter> - </receiver> </application> </manifest> diff --git a/res/drawable/app_launcher_ripple_background.xml b/res/drawable/app_launcher_ripple_background.xml index 33a25d6..450ca8f 100644 --- a/res/drawable/app_launcher_ripple_background.xml +++ b/res/drawable/app_launcher_ripple_background.xml @@ -16,11 +16,11 @@ --> <ripple xmlns:android="http://schemas.android.com/apk/res/android" - android:color="@color/car_card_ripple_background"> + android:color="?android:attr/colorControlHighlight"> <item android:id="@android:id/mask"> <shape android:shape="rectangle"> <solid android:color="@android:color/white" /> - <corners android:radius="@dimen/car_radius_2" /> + <corners android:radius="@dimen/app_icon_ripple_radius" /> </shape> </item> </ripple> diff --git a/res/drawable/ic_partly_cloudy.png b/res/drawable/ic_partly_cloudy.png Binary files differnew file mode 100644 index 0000000..7a5be64 --- /dev/null +++ b/res/drawable/ic_partly_cloudy.png diff --git a/res/layout/app_grid_activity.xml b/res/layout/app_grid_activity.xml index 72538a8..af50bf1 100644 --- a/res/layout/app_grid_activity.xml +++ b/res/layout/app_grid_activity.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - Copyright (C) 2018 The Android Open Source Project + Copyright (C) 2019 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,21 +19,18 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="@color/car_card" + android:layout_height="match_parent" android:orientation="vertical"> <include layout="@layout/app_grid_activity_header" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginBottom="@dimen/car_padding_4" /> + android:layout_height="wrap_content"/> - <androidx.car.widget.PagedListView + <com.android.car.apps.common.widget.PagedRecyclerView android:id="@+id/apps_grid" android:layout_width="match_parent" android:layout_height="match_parent" - app:scrollBarTopMargin="0dp" - app:dayNightStyle="auto" - app:showPagedListViewDivider="false" /> + /> </LinearLayout> + diff --git a/res/layout/app_grid_activity_header.xml b/res/layout/app_grid_activity_header.xml index 4d0ab07..cc38658 100644 --- a/res/layout/app_grid_activity_header.xml +++ b/res/layout/app_grid_activity_header.xml @@ -20,7 +20,6 @@ android:id="@+id/header" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@color/car_card" android:clipChildren="false" android:clipToPadding="false" android:gravity="center_vertical" @@ -28,61 +27,61 @@ <FrameLayout android:id="@+id/exit_button_container" - android:layout_width="@dimen/car_margin" - android:layout_height="@dimen/car_app_bar_height" + android:layout_width="@dimen/panel_margin" + android:layout_height="@dimen/app_bar_height" android:layout_alignParentStart="true" android:layout_centerVertical="true" - android:background="@drawable/car_card_ripple_background" + android:background="?android:attr/selectableItemBackground" android:clickable="true" android:focusable="true"> <ImageButton - android:layout_width="@dimen/car_primary_icon_size" - android:layout_height="@dimen/car_primary_icon_size" + android:layout_width="@dimen/icon_size" + android:layout_height="@dimen/icon_size" android:layout_gravity="center" - android:background="@null" - android:src="@drawable/ic_clear_black" - android:tint="@color/car_tint" - android:scaleType="fitXY" android:adjustViewBounds="true" + android:background="@null" android:clickable="false" - android:focusable="false" /> + android:focusable="false" + android:scaleType="fitXY" + android:src="@drawable/ic_clear_black" + android:tint="@color/icon_tint"/> </FrameLayout> <TextView + style="@style/TitleText" android:layout_width="wrap_content" - android:layout_height="@dimen/car_app_bar_height" + android:layout_height="@dimen/app_bar_height" android:layout_toEndOf="@id/exit_button_container" android:gravity="center_vertical" android:text="@string/all_apps" - android:textAppearance="@style/TextAppearance.Car.Title2" /> + /> <FrameLayout android:id="@+id/search_button_container" - android:layout_width="@dimen/car_margin" - android:layout_height="@dimen/car_touch_target_size" - android:layout_centerVertical="true" + android:layout_width="@dimen/panel_margin" + android:layout_height="@dimen/app_grid_touch_target_size" android:layout_alignParentEnd="true" - android:background="@drawable/car_card_ripple_background" + android:layout_centerVertical="true" + android:background="?android:attr/selectableItemBackground" android:clickable="true" android:focusable="true" - android:visibility="gone"> + android:visibility="gone" + > <ImageButton - android:layout_width="@dimen/car_primary_icon_size" - android:layout_height="@dimen/car_primary_icon_size" + android:layout_width="@dimen/icon_size" + android:layout_height="@dimen/icon_size" android:layout_gravity="center" - android:background="@null" - android:src="@drawable/ic_search_black" - android:tint="@color/car_tint" - android:scaleType="fitXY" android:adjustViewBounds="true" + android:background="@null" android:clickable="false" - android:focusable="false" /> + android:focusable="false" + android:scaleType="fitXY" + android:src="@drawable/ic_search_black" + android:tint="@color/icon_tint"/> </FrameLayout> <View - android:layout_width="match_parent" - android:layout_height="@dimen/car_list_divider_height" - android:layout_alignBottom="@id/exit_button_container" - android:background="@color/car_list_divider"/> + style="@style/HorizontalLineDivider" + android:layout_alignBottom="@id/exit_button_container"/> </RelativeLayout> diff --git a/res/layout/app_item.xml b/res/layout/app_item.xml index ddfe8ff..2e7e011 100644 --- a/res/layout/app_item.xml +++ b/res/layout/app_item.xml @@ -19,26 +19,26 @@ android:id="@+id/app_item" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="@dimen/car_padding_1" - android:layout_marginStart="@dimen/car_padding_2" - android:layout_marginEnd="@dimen/car_padding_2" - android:layout_marginBottom="@dimen/car_padding_5" + android:layout_marginTop="@dimen/app_grid_row_margin" + android:layout_marginBottom="@dimen/app_grid_row_margin" + android:layout_marginEnd="@dimen/app_touch_target_margin" + android:layout_marginStart="@dimen/app_touch_target_margin" + android:background="@drawable/app_launcher_ripple_background" android:gravity="center" android:orientation="vertical" - android:background="@drawable/app_launcher_ripple_background"> + android:padding="@dimen/app_touch_target_padding"> <ImageView android:id="@+id/app_icon" - android:layout_width="@dimen/car_touch_target_size" - android:layout_height="@dimen/car_touch_target_size" - android:layout_marginBottom="@dimen/car_padding_4" - android:layout_gravity="center_horizontal" /> + android:layout_width="@dimen/app_grid_touch_target_size" + android:layout_height="@dimen/app_grid_touch_target_size" + android:layout_gravity="center_horizontal" + android:layout_marginBottom="@dimen/app_icon_description_margin"/> <TextView android:id="@+id/app_name" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textAppearance="@style/TextAppearance.Car.Label1" android:ellipsize="end" - android:maxLines="1" /> + android:maxLines="1"/> </LinearLayout> diff --git a/res/layout/app_search_activity.xml b/res/layout/app_search_activity.xml index 04badf9..6b17641 100644 --- a/res/layout/app_search_activity.xml +++ b/res/layout/app_search_activity.xml @@ -21,60 +21,57 @@ android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/search_activity_background_color" - android:orientation="vertical" - android:gravity="center_horizontal"> + android:background="?android:attr/colorPrimary" + android:gravity="center_horizontal" + android:orientation="vertical"> <RelativeLayout android:id="@+id/app_search_header" android:layout_width="match_parent" - android:layout_height="@dimen/car_app_bar_height"> + android:layout_height="@dimen/app_bar_height"> <FrameLayout android:id="@+id/exit_button_container" - android:layout_width="@dimen/car_margin" - android:layout_height="@dimen/car_touch_target_size" - android:background="@drawable/car_card_ripple_background" + android:layout_width="@dimen/panel_margin" + android:layout_height="@dimen/app_grid_touch_target_size" + android:background="?android:attr/selectableItemBackground" android:clickable="true" android:focusable="true"> <ImageButton - android:layout_width="@dimen/car_primary_icon_size" - android:layout_height="@dimen/car_primary_icon_size" + android:layout_width="@dimen/icon_size" + android:layout_height="@dimen/icon_size" android:layout_gravity="center" + android:adjustViewBounds="true" android:background="@null" + android:clickable="false" + android:focusable="false" android:gravity="center" android:src="@drawable/ic_arrow_back_black" - android:tint="@color/car_tint" - android:adjustViewBounds="true" - android:clickable="false" - android:focusable="false" /> + android:tint="@color/icon_tint"/> </FrameLayout> <EditText android:id="@+id/app_search_bar" android:layout_width="@dimen/search_item_width" android:layout_height="@dimen/search_box_height" - android:layout_gravity="center_vertical" android:layout_centerInParent="true" - android:drawableStart="@drawable/ic_search_black" + android:layout_gravity="center_vertical" + android:background="?android:attr/colorPrimary" android:drawablePadding="@dimen/search_bar_drawable_text_padding" - android:drawableTint="@color/car_tint" - android:background="@drawable/car_card_rounded_background" + android:drawableStart="@drawable/ic_search_black" + android:drawableTint="@color/icon_tint" android:inputType="text|textNoSuggestions" android:maxLines="1" - android:paddingStart="@dimen/car_keyline_1" - android:paddingEnd="@dimen/car_keyline_1" - android:textAppearance="@style/TextAppearance.Car.Body1" /> + android:paddingEnd="@dimen/search_bar_margin" + android:paddingStart="@dimen/search_bar_margin" + android:textAppearance="?android:attr/textAppearanceLarge"/> </RelativeLayout> - <androidx.car.widget.PagedListView + <androidx.recyclerview.widget.RecyclerView android:id="@+id/search_result" android:layout_width="@dimen/search_item_width" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/car_padding_1" - android:background="@drawable/car_card_rounded_background" - app:scrollBarEnabled="false" - app:gutter="none" /> + android:layout_marginTop="@dimen/search_result_margin"/> </LinearLayout> diff --git a/res/layout/app_search_result_item.xml b/res/layout/app_search_result_item.xml index 131db89..5fdfcc1 100644 --- a/res/layout/app_search_result_item.xml +++ b/res/layout/app_search_result_item.xml @@ -20,18 +20,18 @@ android:id="@+id/app_item" android:layout_width="match_parent" android:layout_height="wrap_content" - android:minHeight="@dimen/car_single_line_list_item_height" - android:background="@color/car_card" + android:background="?android:attr/selectableItemBackground" + android:minHeight="@*android:dimen/car_single_line_list_item_height" android:orientation="horizontal"> <ImageView android:id="@+id/app_icon" - android:layout_width="@dimen/car_primary_icon_size" - android:layout_height="@dimen/car_primary_icon_size" + android:layout_width="@dimen/icon_size" + android:layout_height="@dimen/icon_size" android:layout_alignParentStart="true" android:layout_centerVertical="true" - android:layout_marginStart="@dimen/car_keyline_1" - android:layout_marginEnd="@dimen/car_keyline_1" /> + android:layout_marginStart="@dimen/search_bar_margin" + android:layout_marginEnd="@dimen/search_bar_margin" /> <TextView android:id="@+id/app_name" @@ -39,9 +39,9 @@ android:layout_height="wrap_content" android:layout_alignParentStart="true" android:layout_centerVertical="true" - android:layout_marginStart="@dimen/car_keyline_3" - android:layout_marginEnd="@dimen/car_keyline_3" - android:textAppearance="@style/TextAppearance.Car.Body1" + android:layout_marginStart="@dimen/search_result_text_margin" + android:layout_marginEnd="@dimen/search_result_text_margin" + android:textAppearance="?android:attr/textAppearanceLarge" android:ellipsize="end" android:maxLines="1" /> </RelativeLayout> diff --git a/res/layout/car_launcher.xml b/res/layout/car_launcher.xml index 583323b..4b28105 100644 --- a/res/layout/car_launcher.xml +++ b/res/layout/car_launcher.xml @@ -22,35 +22,90 @@ android:layout_height="match_parent" tools:context=".CarLauncher"> - <TextView - android:id="@+id/title" + <androidx.constraintlayout.widget.Guideline + android:id="@+id/start_edge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_begin="@dimen/horizontal_border_size"/> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/top_edge" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/title_text" - android:textSize="40sp" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintLeft_toLeftOf="parent" - app:layout_constraintRight_toRightOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <ImageButton - android:id="@+id/appsButton" - android:layout_width="@dimen/button_size" - android:layout_height="@dimen/button_size" - android:layout_marginTop="32dp" - android:background="?android:attr/selectableItemBackground" - android:src="@drawable/ic_apps_black" - android:onClick="openAppsList" - app:layout_constraintEnd_toStartOf="@+id/centerLine" - app:layout_constraintStart_toStartOf="@+id/centerLine" - app:layout_constraintTop_toBottomOf="@+id/title" - /> + android:orientation="horizontal" + app:layout_constraintGuide_begin="@dimen/vertical_border_size"/> <androidx.constraintlayout.widget.Guideline - android:id="@+id/centerLine" + android:id="@+id/end_edge" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" - app:layout_constraintGuide_percent="0.5" /> + app:layout_constraintGuide_end="@dimen/horizontal_border_size"/> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/bottom_edge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_end="@dimen/vertical_border_size"/> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/divider_vertical" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_percent="@dimen/maps_screen_percentage"/> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/divider_horizontal" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_percent="@dimen/contextual_screen_percentage"/> + + <View + android:id="@+id/top_line" + style="@style/HorizontalLineDivider" + app:layout_constraintTop_toTopOf="parent"/> + + <androidx.cardview.widget.CardView + style="@style/CardViewStyle" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_margin="@dimen/main_screen_widget_margin" + app:layout_constraintBottom_toTopOf="@+id/bottom_edge" + app:layout_constraintEnd_toStartOf="@+id/divider_vertical" + app:layout_constraintStart_toEndOf="@+id/start_edge" + app:layout_constraintTop_toBottomOf="@+id/top_edge"> + <ActivityView + android:id="@+id/maps" + android:layout_width="match_parent" + android:layout_height="match_parent"/> + </androidx.cardview.widget.CardView> + + <FrameLayout + android:id="@+id/contextual" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_margin="@dimen/main_screen_widget_margin" + app:layout_constraintBottom_toTopOf="@+id/divider_horizontal" + app:layout_constraintEnd_toStartOf="@+id/end_edge" + app:layout_constraintStart_toEndOf="@+id/divider_vertical" + app:layout_constraintTop_toBottomOf="@+id/top_edge"/> + + <FrameLayout + android:id="@+id/playback" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_margin="@dimen/main_screen_widget_margin" + app:layout_constraintBottom_toTopOf="@+id/bottom_edge" + app:layout_constraintEnd_toStartOf="@+id/end_edge" + app:layout_constraintStart_toEndOf="@+id/divider_vertical" + app:layout_constraintTop_toBottomOf="@+id/divider_horizontal"/> + <View + android:id="@+id/bottom_line" + style="@style/HorizontalLineDivider" + app:layout_constraintBottom_toBottomOf="parent"/> -</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/res/layout/car_launcher_multiwindow.xml b/res/layout/car_launcher_multiwindow.xml new file mode 100644 index 0000000..04924be --- /dev/null +++ b/res/layout/car_launcher_multiwindow.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".CarLauncher"> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/start_edge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_begin="@dimen/horizontal_border_size"/> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/top_edge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_begin="@dimen/vertical_border_size"/> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/end_edge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_end="@dimen/horizontal_border_size"/> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/bottom_edge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_end="@dimen/vertical_border_size"/> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/divider_horizontal" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_percent="@dimen/contextual_screen_percentage"/> + + <View + android:id="@+id/top_line" + style="@style/HorizontalLineDivider" + app:layout_constraintTop_toTopOf="parent"/> + + <FrameLayout + android:id="@+id/contextual" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_margin="@dimen/main_screen_widget_margin" + app:layout_constraintBottom_toTopOf="@+id/divider_horizontal" + app:layout_constraintEnd_toStartOf="@+id/end_edge" + app:layout_constraintStart_toEndOf="@+id/start_edge" + app:layout_constraintTop_toBottomOf="@+id/top_edge"/> + + <FrameLayout + android:id="@+id/playback" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_margin="@dimen/main_screen_widget_margin" + app:layout_constraintBottom_toTopOf="@+id/bottom_edge" + app:layout_constraintEnd_toStartOf="@+id/end_edge" + app:layout_constraintStart_toEndOf="@+id/start_edge" + app:layout_constraintTop_toBottomOf="@+id/divider_horizontal"/> + <View + android:id="@+id/bottom_line" + style="@style/HorizontalLineDivider" + app:layout_constraintBottom_toBottomOf="parent"/> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/res/layout/contextual_fragment.xml b/res/layout/contextual_fragment.xml new file mode 100644 index 0000000..3f090f5 --- /dev/null +++ b/res/layout/contextual_fragment.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2018, The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<androidx.cardview.widget.CardView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + style="@style/ContextualSpace" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical"> + + <ImageView + android:id="@+id/icon" + android:layout_width="80dp" + android:layout_height="80dp" + android:layout_marginStart="@dimen/icon_start_margin" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent"/> + + <TextView + android:id="@+id/top_line" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/icon_end_margin" + android:ellipsize="end" + android:maxLines="1" + android:textAppearance="?android:attr/textAppearanceLarge" + app:layout_constraintStart_toEndOf="@+id/icon" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@+id/bottom_line_container" + app:layout_constraintVertical_chainStyle="packed"/> + + <LinearLayout + android:id="@+id/bottom_line_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/icon_end_margin" + android:layout_marginTop="@dimen/line_gap_margin" + android:baselineAligned="true" + app:layout_constraintStart_toEndOf="@+id/icon" + app:layout_constraintTop_toBottomOf="@+id/top_line" + app:layout_constraintBottom_toBottomOf="parent"> + + <TextView + android:id="@+id/bottom_line" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="1" + android:textAppearance="?android:attr/textAppearanceLarge"/> + + <TextView + android:id="@+id/date_divider" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="4dp" + android:layout_marginEnd="6dp" + android:text="│" + android:textAppearance="?android:attr/textAppearanceLarge" + android:textColor="@color/date_divider_bar_color"/> + + <com.android.car.carlauncher.LocalizedTextClock + android:id="@+id/date" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:format12Hour="@string/date" + android:format24Hour="@string/date" + android:textAppearance="?android:attr/textAppearanceLarge"/> + + </LinearLayout> + + </androidx.constraintlayout.widget.ConstraintLayout> + +</androidx.cardview.widget.CardView> diff --git a/res/layout/recent_apps_row.xml b/res/layout/recent_apps_row.xml index 3848aff..7c58de1 100644 --- a/res/layout/recent_apps_row.xml +++ b/res/layout/recent_apps_row.xml @@ -16,9 +16,9 @@ --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:orientation="vertical" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:orientation="vertical"> <LinearLayout android:id="@+id/recent_apps_row" @@ -29,8 +29,7 @@ <View android:id="@+id/divider" android:layout_width="match_parent" - android:layout_height="@dimen/car_list_divider_height" - android:layout_marginTop="@dimen/recent_apps_divider_margin" - android:layout_marginBottom="@dimen/recent_apps_divider_margin" - android:background="@color/car_list_divider"/> + android:layout_height="@dimen/recent_apps_row_height" + android:layout_marginTop="@dimen/app_grid_row_margin" + android:background="@color/recent_apps_line_divider_color"/> </LinearLayout>
\ No newline at end of file diff --git a/res/values-night/colors.xml b/res/values-night/colors.xml deleted file mode 100644 index 5019fbc..0000000 --- a/res/values-night/colors.xml +++ /dev/null @@ -1,21 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2017 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<resources> - <color name="title_text_color">#FFFFFF</color> - <color name="list_item_bg">#000000</color> - <color name="list_item_text_color">#bdbdbd</color> - <color name="search_activity_background_color">#ff222222</color> -</resources> diff --git a/res/values/colors.xml b/res/values/colors.xml index 9519eac..b17ae79 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -14,8 +14,7 @@ limitations under the License. --> <resources> - <color name="title_text_color">#FFFFFF</color> - <color name="list_item_bg">#bdbdbd</color> - <color name="list_item_text_color">#424242</color> - <color name="search_activity_background_color">#ffeeeeee</color> + <color name="date_divider_bar_color">@*android:color/car_grey_500</color> + <color name="icon_tint">@*android:color/car_tint</color> + <color name="recent_apps_line_divider_color">@*android:color/car_list_divider</color> </resources> diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 8a3fe69..4c276a4 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -15,19 +15,49 @@ limitations under the License. --> <resources> + <!-- CarLauncher Activity values --> + <dimen name="launcher_card_corner_radius">6dp</dimen> + <item name="maps_screen_percentage" type="dimen" format="float">0.6</item> + <!-- Vertical percentage of screen (not occupied by maps to devote to the contextual space + (Ex: date time temp) --> + <item name="contextual_screen_percentage" type="dimen" format="float">.3</item> + <!--Used for the space between the top, and bottom of the screen and the content --> + <dimen name="vertical_border_size">@*android:dimen/car_padding_1</dimen> + <!-- Used for the space between the start, and end of the screen and the content --> + <dimen name="horizontal_border_size">@*android:dimen/car_padding_1</dimen> + <!-- Margin surrounding fragments on the main activity --> + <dimen name="main_screen_widget_margin">@*android:dimen/car_padding_1</dimen> + <!-- Margin between the edge of the card and start of the icon --> + <dimen name="icon_start_margin">@*android:dimen/car_padding_4</dimen> + <!-- Gap between the end of the icon and the start of the text --> + <dimen name="icon_end_margin">@*android:dimen/car_padding_3</dimen> + <!-- Gap between text lines --> + <dimen name="line_gap_margin">@*android:dimen/car_padding_1</dimen> + + <!-- AppGridActivity --> <dimen name="recent_apps_divider_margin">40dp</dimen> + <item name="app_icon_opacity_unavailable" type="dimen" format="float">0.5</item> + <item name="app_icon_opacity" type="dimen" format="float">1.0</item> + <!-- Gap between top of screen (statusbar) and header content --> + <dimen name="app_grid_header_margin">@*android:dimen/car_padding_4</dimen> + <dimen name="panel_margin">@*android:dimen/car_margin</dimen> + <!-- Height of the text and exit button on the app selection screen --> + <dimen name="app_bar_height">@*android:dimen/car_app_bar_height</dimen> + <dimen name="icon_size">@*android:dimen/car_primary_icon_size</dimen> + <dimen name="recent_apps_row_height">@*android:dimen/car_list_divider_height</dimen> + <dimen name="app_grid_touch_target_size">@*android:dimen/car_touch_target_size</dimen> + <!-- Padding around the touch target (makes ripple look better) --> + <dimen name="app_touch_target_padding">@*android:dimen/car_padding_1</dimen> + <dimen name="app_touch_target_margin">@*android:dimen/car_padding_2</dimen> + <dimen name="app_grid_row_margin">@*android:dimen/car_padding_4</dimen> + <dimen name="app_icon_description_margin">@*android:dimen/car_padding_4</dimen> + <dimen name="app_icon_ripple_radius">@*android:dimen/car_radius_2</dimen> + <!-- AppSearchActivity --> <dimen name="search_item_width">760dp</dimen> + <dimen name="search_result_margin">@*android:dimen/car_padding_1</dimen> + <dimen name="search_result_text_margin">@*android:dimen/car_keyline_3</dimen> <dimen name="search_box_height">65dp</dimen> + <dimen name="search_bar_margin">@*android:dimen/car_keyline_1</dimen> <dimen name="search_bar_drawable_text_padding">46dp</dimen> - - <dimen name="tile_text_size">56sp</dimen> - <dimen name="title_text_margin_bottom">96dp</dimen> - <dimen name="button_size">96dp</dimen> - - <dimen name="list_item_text_size">32sp</dimen> - <dimen name="list_item_icon_size">96dp</dimen> - <dimen name="list_item_vertical_padding">32dp</dimen> - <dimen name="list_item_marginStart">24dp</dimen> - <dimen name="list_item_text_margin_start">105dp</dimen> </resources> diff --git a/res/values/strings.xml b/res/values/strings.xml index ab544c6..57e5376 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -14,8 +14,59 @@ See the License for the specific language governing permissions and limitations under the License. --> -<resources> - <string name="title_text">Let\'s Drive</string> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="app_title">Car Launcher</string> <string name="all_apps">All apps</string> -</resources>
\ No newline at end of file + + <string name="driving_toast_text"> + <xliff:g id="app_name" example="Settings">%1$s</xliff:g> can\'t be used while driving. + </string> + + <!-- Contextual Fragment --> + <string name="greeting">Hi, + <xliff:g example="Owner" id="user">%s</xliff:g> + </string> + <string name="temperature" translatable="false">%d\u00B0</string> + <string name="temperature_empty" translatable="false">--\u00B0</string> + <string name="date" translatable="false">EEEMMMd</string> + + <plurals name="projection_devices"> + <item quantity="one"><xliff:g example="1" id="count">%d</xliff:g> device</item> + <item quantity="other"><xliff:g example="2" id="count">%d</xliff:g> devices</item> + </plurals> + + <!-- + A list of package names to exclude from the app selector when filtering is active. + DO NOT TRANSLATE. + --> + <string-array name="hidden_apps" translatable="false"> + <item>com.android.car.themeplayground</item> + <item>com.google.android.auto.welcome</item> + <item>com.android.car.acast.source</item> + <item>com.google.android.embedded.projection</item> + <item>com.google.android.car.bugreport</item> + <item>com.android.car.trust</item> + <item>com.android.car.datacenter</item> + <item>com.google.android.car.diagnosticrecorder</item> + <item>com.google.android.car.diskwriteapp</item> + <item>com.android.documentsui</item> + <item>com.google.android.car.flashapp</item> + <item>com.android.gallery3d</item> + <item>com.google.android.car.garagemode.testapp</item> + <item>com.google.android.gms.vehicle.testapp</item> + <item>com.google.android.car.kitchensink</item> + <item>com.android.support.car.lenspicker</item> + <item>com.google.android.apps.gmm</item> + <item>com.android.car.media</item> + <item>com.qualcomm.qti.sensors.qsensortest</item> + <item>com.qualcomm.qti.logkit</item> + <item>com.qualcomm.qti.roamingsettings</item> + <item>com.android.settings</item> + <item>com.google.android.car.uxr.sample</item> + <item>com.google.android.apps.geo.autograph.vms.client.visualizer.car</item> + <item>com.google.android.apps.geo.autograph.vms.client.systemstate</item> + <item>com.google.android.car.vms.subscriber</item> + <item>org.chromium.webview_shell</item> + </string-array> + +</resources> diff --git a/res/values/styles.xml b/res/values/styles.xml new file mode 100644 index 0000000..c576b1c --- /dev/null +++ b/res/values/styles.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources> + <style name="CardViewStyle"> + <item name="cardCornerRadius">@dimen/launcher_card_corner_radius</item> + <item name="cardElevation">0dp</item> + </style> + + <style name="ContextualSpace" parent="CardViewStyle"> + <item name="cardBackgroundColor">?android:attr/colorPrimary</item> + </style> + + <style name="TitleText"> + <item name="android:textAppearance">?android:attr/textAppearanceLarge</item> + <item name="android:fontFamily">sans-serif-medium</item> + </style> + + <style name="HorizontalLineDivider"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">@*android:dimen/car_list_divider_height</item> + <item name="android:background">@*android:color/car_list_divider</item> + </style> +</resources> diff --git a/res/values/themes.xml b/res/values/themes.xml new file mode 100644 index 0000000..5422778 --- /dev/null +++ b/res/values/themes.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2018 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License + --> + +<resources> + <!--Theme for the app, it's defined empty here so it can be overlaid easily --> + <style name="Theme.Launcher" parent="android:Theme.DeviceDefault.Wallpaper.NoTitleBar"> + <item name="textAppearanceGridItem">@android:style/TextAppearance.DeviceDefault.Medium</item> + <item name="textAppearanceGridItemSecondary">@android:style/TextAppearance.DeviceDefault.Small</item> + </style> +</resources> diff --git a/src/com/android/car/carlauncher/AppGridActivity.java b/src/com/android/car/carlauncher/AppGridActivity.java index 62d0699..a4a8bed 100644 --- a/src/com/android/car/carlauncher/AppGridActivity.java +++ b/src/com/android/car/carlauncher/AppGridActivity.java @@ -30,10 +30,8 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; -import android.content.pm.ActivityInfo; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; -import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.IBinder; import android.text.TextUtils; @@ -44,12 +42,16 @@ import android.view.View; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup; +import androidx.recyclerview.widget.RecyclerView; +import com.android.car.carlauncher.AppLauncherUtils.LauncherAppsInfo; + import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.List; - -import androidx.car.widget.PagedListView; +import java.util.Set; /** * Launcher activity that shows a grid of apps. @@ -59,6 +61,8 @@ public final class AppGridActivity extends Activity { private static final String TAG = "AppGridActivity"; private int mColumnNumber; + private boolean mShowAllApps = true; + private final Set<String> mHiddenApps = new HashSet<>(); private AppGridAdapter mGridAdapter; private PackageManager mPackageManager; private UsageStatsManager mUsageStatsManager; @@ -83,9 +87,7 @@ public final class AppGridActivity extends Activity { restrictionInfo.isRequiresDistractionOptimization())); mCarPackageManager = (CarPackageManager) mCar.getCarManager(Car.PACKAGE_SERVICE); - mGridAdapter.setAllApps(getAllApps()); - mGridAdapter.setMostRecentApps(getMostRecentApps()); - + updateAppsLists(); } catch (CarNotConnectedException e) { Log.e(TAG, "Car not connected in CarConnectionListener", e); } @@ -105,10 +107,20 @@ public final class AppGridActivity extends Activity { mPackageManager = getPackageManager(); mUsageStatsManager = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE); mCar = Car.createCar(this, mCarConnectionListener); + mHiddenApps.addAll(Arrays.asList(getResources().getStringArray(R.array.hidden_apps))); setContentView(R.layout.app_grid_activity); - findViewById(R.id.exit_button_container).setOnClickListener(v -> finish()); + View exitView = findViewById(R.id.exit_button_container); + exitView.setOnClickListener(v -> finish()); + exitView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + mShowAllApps = !mShowAllApps; + updateAppsLists(); + return true; + } + }); findViewById(R.id.search_button_container).setOnClickListener((View view) -> { Intent intent = new Intent(this, AppSearchActivity.class); @@ -116,7 +128,7 @@ public final class AppGridActivity extends Activity { }); mGridAdapter = new AppGridAdapter(this); - PagedListView gridView = findViewById(R.id.apps_grid); + RecyclerView gridView = findViewById(R.id.apps_grid); GridLayoutManager gridLayoutManager = new GridLayoutManager(this, mColumnNumber); gridLayoutManager.setSpanSizeLookup(new SpanSizeLookup() { @@ -125,7 +137,7 @@ public final class AppGridActivity extends Activity { return mGridAdapter.getSpanSizeLookup(position); } }); - gridView.getRecyclerView().setLayoutManager(gridLayoutManager); + gridView.setLayoutManager(gridLayoutManager); gridView.setAdapter(mGridAdapter); } @@ -133,10 +145,18 @@ public final class AppGridActivity extends Activity { @Override protected void onResume() { super.onResume(); - mGridAdapter.setAllApps(getAllApps()); - // using onResume() to refresh most recently used apps because we want to refresh even if + // Using onResume() to refresh most recently used apps because we want to refresh even if // the app being launched crashes/doesn't cover the entire screen. - mGridAdapter.setMostRecentApps(getMostRecentApps()); + updateAppsLists(); + } + + /** Updates the list of all apps, and the list of the most recently used ones. */ + private void updateAppsLists() { + Set<String> blackList = mShowAllApps ? Collections.emptySet() : mHiddenApps; + LauncherAppsInfo appsInfo = AppLauncherUtils.getAllLauncherApps(blackList, + getSystemService(LauncherApps.class), mCarPackageManager, mPackageManager); + mGridAdapter.setAllApps(appsInfo.getApplicationsList()); + mGridAdapter.setMostRecentApps(getMostRecentApps(appsInfo)); } @Override @@ -177,8 +197,15 @@ public final class AppGridActivity extends Activity { } } - private List<AppMetaData> getMostRecentApps() { + /** + * Note that in order to obtain usage stats from the previous boot, + * the device must have gone through a clean shut down process. + */ + private List<AppMetaData> getMostRecentApps(LauncherAppsInfo appsInfo) { ArrayList<AppMetaData> apps = new ArrayList<>(); + if (appsInfo.isEmpty()) { + return apps; + } // get the usage stats starting from 1 year ago with a INTERVAL_YEARLY granularity // returning entries like: @@ -195,14 +222,15 @@ public final class AppGridActivity extends Activity { return apps; // empty list } - Collections.sort(stats, new LastTimeUsedComparator()); + stats.sort(new LastTimeUsedComparator()); int currentIndex = 0; int itemsAdded = 0; int statsSize = stats.size(); int itemCount = Math.min(mColumnNumber, statsSize); while (itemsAdded < itemCount && currentIndex < statsSize) { - String packageName = stats.get(currentIndex).getPackageName(); + UsageStats usageStats = stats.get(currentIndex); + String packageName = usageStats.mPackageName; currentIndex++; // do not include self @@ -210,36 +238,26 @@ public final class AppGridActivity extends Activity { continue; } - // do not include apps that don't support starting from launcher - Intent intent = getPackageManager().getLaunchIntentForPackage(packageName); - if (intent == null || !intent.hasCategory(Intent.CATEGORY_LAUNCHER)) { - continue; - } - - try { - // try getting activity info from package name - Drawable icon = mPackageManager.getActivityIcon(intent); - ActivityInfo info = mPackageManager.getActivityInfo(intent.getComponent(), 0); - CharSequence displayName = info.loadLabel(mPackageManager); - if (icon == null || TextUtils.isEmpty(displayName)) { + // Exempt media services from background and launcher checks + if (!appsInfo.isMediaService(packageName)) { + // do not include apps that only ran in the background + if (usageStats.getTotalTimeInForeground() == 0) { continue; } - boolean isDistractionOptimized = AppLauncherUtils.isActivityDistractionOptimized( - mCarPackageManager, packageName, intent.getComponent().getClassName()); - AppMetaData app = - new AppMetaData(displayName, packageName, icon, isDistractionOptimized); - - // edge case: do not include duplicated entries - // e.g. app is used at 2017/12/31 23:59, and 2018/01/01 00:00 - if (apps.contains(app)) { + + // do not include apps that don't support starting from launcher + Intent intent = getPackageManager().getLaunchIntentForPackage(packageName); + if (intent == null || !intent.hasCategory(Intent.CATEGORY_LAUNCHER)) { continue; } + } + AppMetaData app = appsInfo.getAppMetaData(packageName); + // Prevent duplicated entries + // e.g. app is used at 2017/12/31 23:59, and 2018/01/01 00:00 + if (app != null && !apps.contains(app)) { apps.add(app); itemsAdded++; - } catch (PackageManager.NameNotFoundException e) { - // this should never happen - Log.e(TAG, "NameNotFoundException when getting app icon in AppGridActivity", e); } } return apps; @@ -258,11 +276,6 @@ public final class AppGridActivity extends Activity { } } - private List<AppMetaData> getAllApps() { - return AppLauncherUtils.getAllLaunchableApps( - getSystemService(LauncherApps.class), mCarPackageManager, mPackageManager); - } - private class AppInstallUninstallReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { @@ -273,7 +286,7 @@ public final class AppGridActivity extends Activity { return; } - mGridAdapter.setAllApps(getAllApps()); + updateAppsLists(); } } } diff --git a/src/com/android/car/carlauncher/AppGridAdapter.java b/src/com/android/car/carlauncher/AppGridAdapter.java index 7b1be64..89cf10a 100644 --- a/src/com/android/car/carlauncher/AppGridAdapter.java +++ b/src/com/android/car/carlauncher/AppGridAdapter.java @@ -25,7 +25,6 @@ import android.view.ViewGroup; import androidx.recyclerview.widget.RecyclerView; import java.util.Collections; -import java.util.Comparator; import java.util.List; /** @@ -61,7 +60,7 @@ final class AppGridAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> notifyDataSetChanged(); } - void setAllApps(List<AppMetaData> apps) { + void setAllApps(@Nullable List<AppMetaData> apps) { mApps = apps; sortAllApps(); notifyDataSetChanged(); diff --git a/src/com/android/car/carlauncher/AppItemViewHolder.java b/src/com/android/car/carlauncher/AppItemViewHolder.java index 3007092..ff3b651 100644 --- a/src/com/android/car/carlauncher/AppItemViewHolder.java +++ b/src/com/android/car/carlauncher/AppItemViewHolder.java @@ -18,9 +18,11 @@ package com.android.car.carlauncher; import android.annotation.Nullable; import android.content.Context; +import android.content.Intent; import android.view.View; import android.widget.ImageView; import android.widget.TextView; +import android.widget.Toast; import androidx.recyclerview.widget.RecyclerView; @@ -29,11 +31,11 @@ import androidx.recyclerview.widget.RecyclerView; */ public class AppItemViewHolder extends RecyclerView.ViewHolder { private final Context mContext; - public View mAppItem; - public ImageView mAppIconView; - public TextView mAppNameView; + private View mAppItem; + private ImageView mAppIconView; + private TextView mAppNameView; - public AppItemViewHolder(View view, Context context) { + AppItemViewHolder(View view, Context context) { super(view); mContext = context; mAppItem = view.findViewById(R.id.app_item); @@ -48,7 +50,6 @@ public class AppItemViewHolder extends RecyclerView.ViewHolder { */ public void bind(@Nullable AppMetaData app, boolean isDistractionOptimizationRequired) { // Empty out the view - mAppItem.setClickable(false); mAppIconView.setImageDrawable(null); mAppNameView.setText(null); @@ -57,11 +58,30 @@ public class AppItemViewHolder extends RecyclerView.ViewHolder { } mAppNameView.setText(app.getDisplayName()); - if (isDistractionOptimizationRequired && !app.getIsDistractionOptimized()) { - mAppIconView.setImageDrawable(AppLauncherUtils.toGrayscale(app.getIcon())); - } else { - mAppIconView.setImageDrawable(app.getIcon()); + mAppIconView.setImageDrawable(app.getIcon()); + boolean isLaunchable = + !isDistractionOptimizationRequired || app.getIsDistractionOptimized(); + mAppIconView.setAlpha(mContext.getResources().getFloat( + isLaunchable ? R.dimen.app_icon_opacity : R.dimen.app_icon_opacity_unavailable)); + + if (isLaunchable) { mAppItem.setOnClickListener(v -> AppLauncherUtils.launchApp(mContext, app)); + mAppItem.setLongClickable(app.getAlternateLaunchIntent() != null); + mAppItem.setOnLongClickListener(v-> openAlternateLaunchIntent(app)); + } else { + String warningText = mContext.getResources() + .getString(R.string.driving_toast_text, app.getDisplayName()); + mAppItem.setOnClickListener( + v -> Toast.makeText(mContext, warningText, Toast.LENGTH_LONG).show()); + } + } + + private boolean openAlternateLaunchIntent(AppMetaData app) { + Intent intent = app.getAlternateLaunchIntent(); + if (intent != null) { + mContext.startActivity(intent); + return true; } + return false; } } diff --git a/src/com/android/car/carlauncher/AppLauncherUtils.java b/src/com/android/car/carlauncher/AppLauncherUtils.java index 30e4ae6..dbeff4d 100644 --- a/src/com/android/car/carlauncher/AppLauncherUtils.java +++ b/src/com/android/car/carlauncher/AppLauncherUtils.java @@ -17,6 +17,8 @@ package com.android.car.carlauncher; import android.annotation.Nullable; +import android.app.ActivityOptions; +import android.car.Car; import android.car.CarNotConnectedException; import android.car.content.pm.CarPackageManager; import android.content.Context; @@ -24,15 +26,20 @@ import android.content.Intent; import android.content.pm.LauncherActivityInfo; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; -import android.graphics.ColorMatrix; -import android.graphics.ColorMatrixColorFilter; -import android.graphics.drawable.Drawable; +import android.content.pm.ResolveInfo; import android.os.Process; +import android.service.media.MediaBrowserService; import android.util.Log; +import androidx.annotation.NonNull; + import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; /** * Util class that contains helper method used by app launcher classes. @@ -57,65 +64,128 @@ class AppLauncherUtils { * @param app the requesting app's AppMetaData */ static void launchApp(Context context, AppMetaData app) { - Intent intent = - context.getPackageManager().getLaunchIntentForPackage(app.getPackageName()); - context.startActivity(intent); + ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(context.getDisplayId()); + context.startActivity(app.getMainLaunchIntent(), options.toBundle()); } - /** - * Converts a {@link Drawable} to grey scale. - * - * @param drawable the original drawable - * @return the grey scale drawable - */ - static Drawable toGrayscale(Drawable drawable) { - ColorMatrix matrix = new ColorMatrix(); - matrix.setSaturation(0); - ColorMatrixColorFilter filter = new ColorMatrixColorFilter(matrix); - // deep copy the original drawable - Drawable newDrawable = drawable.getConstantState().newDrawable().mutate(); - newDrawable.setColorFilter(filter); - return newDrawable; + /** Bundles application and services info. */ + static class LauncherAppsInfo { + /** Map of all apps' metadata keyed by package name. */ + private final Map<String, AppMetaData> mApplications; + + /** Map of all the media services keyed by package name. */ + private final Map<String, ResolveInfo> mMediaServices; + + LauncherAppsInfo(@NonNull Map<String, AppMetaData> apps, + @NonNull Map<String, ResolveInfo> mediaServices) { + mApplications = apps; + mMediaServices = mediaServices; + } + + /** Returns true if all maps are empty. */ + boolean isEmpty() { + return mApplications.isEmpty() && mMediaServices.isEmpty(); + } + + /** Returns whether the given package name is a media service. */ + boolean isMediaService(String packageName) { + return mMediaServices.containsKey(packageName); + } + + /** Returns the {@link AppMetaData} for the given package name. */ + @Nullable + AppMetaData getAppMetaData(String packageName) { + return mApplications.get(packageName); + } + + /** Returns a new list of the applications' {@link AppMetaData}. */ + @NonNull + List<AppMetaData> getApplicationsList() { + return new ArrayList<>(mApplications.values()); + } } + private final static LauncherAppsInfo EMPTY_APPS_INFO = new LauncherAppsInfo( + Collections.emptyMap(), Collections.emptyMap()); + /** - * Gets all apps that support launching from launcher in unsorted order. + * Gets all the apps that we want to see in the launcher in unsorted order. Includes media + * services without launcher activities. * + * @param blackList A (possibly empty) list of apps to hide * @param launcherApps The {@link LauncherApps} system service * @param carPackageManager The {@link CarPackageManager} system service * @param packageManager The {@link PackageManager} system service - * @return a list of all apps' metadata + * @return a new {@link LauncherAppsInfo} */ - @Nullable - static List<AppMetaData> getAllLaunchableApps( + @NonNull + static LauncherAppsInfo getAllLauncherApps( + @NonNull Set<String> blackList, LauncherApps launcherApps, CarPackageManager carPackageManager, PackageManager packageManager) { if (launcherApps == null || carPackageManager == null || packageManager == null) { - return null; + return EMPTY_APPS_INFO; } - List<AppMetaData> apps = new ArrayList<>(); - - Intent intent = new Intent(Intent.ACTION_MAIN, null); - intent.addCategory(Intent.CATEGORY_LAUNCHER); - + List<ResolveInfo> mediaServices = packageManager.queryIntentServices( + new Intent(MediaBrowserService.SERVICE_INTERFACE), + PackageManager.GET_RESOLVED_FILTER); List<LauncherActivityInfo> availableActivities = launcherApps.getActivityList(null, Process.myUserHandle()); + + Map<String, AppMetaData> apps = new HashMap<>( + mediaServices.size() + availableActivities.size()); + Map<String, ResolveInfo> mediaServicesMap = new HashMap<>(mediaServices.size()); + + // Process media services + for (ResolveInfo info : mediaServices) { + String packageName = info.serviceInfo.packageName; + mediaServicesMap.put(packageName, info); + if (shouldAdd(packageName, apps, blackList)) { + final boolean isDistractionOptimized = true; + + Intent intent = new Intent(Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE); + intent.putExtra(Car.CAR_EXTRA_MEDIA_PACKAGE, packageName); + + AppMetaData appMetaData = new AppMetaData( + info.serviceInfo.loadLabel(packageManager), + packageName, + info.serviceInfo.loadIcon(packageManager), + isDistractionOptimized, + intent, + packageManager.getLaunchIntentForPackage(packageName)); + apps.put(packageName, appMetaData); + } + } + + // Process activities for (LauncherActivityInfo info : availableActivities) { String packageName = info.getComponentName().getPackageName(); - boolean isDistractionOptimized = - isActivityDistractionOptimized(carPackageManager, packageName, info.getName()); - - AppMetaData app = new AppMetaData( - info.getLabel(), - packageName, - info.getApplicationInfo().loadIcon(packageManager), - isDistractionOptimized); - apps.add(app); + if (shouldAdd(packageName, apps, blackList)) { + boolean isDistractionOptimized = + isActivityDistractionOptimized(carPackageManager, packageName, + info.getName()); + + AppMetaData appMetaData = new AppMetaData( + info.getLabel(), + packageName, + info.getBadgedIcon(0), + isDistractionOptimized, + packageManager.getLaunchIntentForPackage(packageName), + null); + apps.put(packageName, appMetaData); + } } - return apps; + + return new LauncherAppsInfo(apps, mediaServicesMap); + } + + private static boolean shouldAdd(String packageName, Map<String, AppMetaData> apps, + @NonNull Set<String> blackList) { + return !apps.containsKey(packageName) && !blackList.contains(packageName); } /** diff --git a/src/com/android/car/carlauncher/AppMetaData.java b/src/com/android/car/carlauncher/AppMetaData.java index ac8a744..affc472 100644 --- a/src/com/android/car/carlauncher/AppMetaData.java +++ b/src/com/android/car/carlauncher/AppMetaData.java @@ -17,30 +17,48 @@ package com.android.car.carlauncher; import android.annotation.Nullable; +import android.content.Intent; import android.graphics.drawable.Drawable; /** - * Meta data of an app including the display name, the full package name, and the icon drawable. + * Meta data of an app including the display name, the full package name, the icon drawable, and an + * intent to either open the app or the media center (for media services). */ final class AppMetaData { // The display name of the app @Nullable - private String mDisplayName; + private final String mDisplayName; // The package name of the app - private String mPackageName; - private Drawable mIcon; - private boolean mIsDistractionOptimized; + private final String mPackageName; + private final Drawable mIcon; + private final boolean mIsDistractionOptimized; + private final Intent mMainLaunchIntent; + private final Intent mAlternateLaunchIntent; - public AppMetaData( + /** + * AppMetaData + * @param displayName the name to display in the launcher + * @param packageName the application's package + * @param icon the application's icon + * @param isDistractionOptimized whether mainLaunchIntent is safe for driving + * @param mainLaunchIntent what to open by default (goes to the media center for media apps) + * @param alternateLaunchIntent temporary allowance for media apps that still need to show UI + * beyond sign in and settings + */ + AppMetaData( CharSequence displayName, String packageName, Drawable icon, - boolean isDistractionOptimized) { + boolean isDistractionOptimized, + Intent mainLaunchIntent, + @Nullable Intent alternateLaunchIntent) { mDisplayName = displayName == null ? "" : displayName.toString(); mPackageName = packageName == null ? "" : packageName; mIcon = icon; mIsDistractionOptimized = isDistractionOptimized; + mMainLaunchIntent = mainLaunchIntent; + mAlternateLaunchIntent = alternateLaunchIntent; } public String getDisplayName() { @@ -51,11 +69,19 @@ final class AppMetaData { return mPackageName; } + Intent getMainLaunchIntent() { + return mMainLaunchIntent; + } + + Intent getAlternateLaunchIntent() { + return mAlternateLaunchIntent; + } + public Drawable getIcon() { return mIcon; } - public boolean getIsDistractionOptimized() { + boolean getIsDistractionOptimized() { return mIsDistractionOptimized; } diff --git a/src/com/android/car/carlauncher/AppSearchActivity.java b/src/com/android/car/carlauncher/AppSearchActivity.java index 77c1577..59315db 100644 --- a/src/com/android/car/carlauncher/AppSearchActivity.java +++ b/src/com/android/car/carlauncher/AppSearchActivity.java @@ -39,10 +39,10 @@ import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; +import androidx.recyclerview.widget.RecyclerView; +import java.util.Collections; import java.util.List; -import androidx.car.widget.PagedListView; - /** * Activity that allows user to search in apps. */ @@ -100,7 +100,7 @@ public final class AppSearchActivity extends Activity { }); findViewById(R.id.exit_button_container).setOnClickListener(view -> finish()); - PagedListView searchResultView = findViewById(R.id.search_result); + RecyclerView searchResultView = findViewById(R.id.search_result); searchResultView.setClipToOutline(true); mSearchResultAdapter = new SearchResultAdapter(this); searchResultView.setAdapter(mSearchResultAdapter); @@ -167,8 +167,10 @@ public final class AppSearchActivity extends Activity { } private List<AppMetaData> getAllApps() { - return AppLauncherUtils.getAllLaunchableApps( - getSystemService(LauncherApps.class), mCarPackageManager, mPackageManager); + AppLauncherUtils.LauncherAppsInfo appsInfo = AppLauncherUtils.getAllLauncherApps( + Collections.emptySet(), getSystemService(LauncherApps.class), mCarPackageManager, + mPackageManager); + return appsInfo.getApplicationsList(); } public void hideKeyboard() { diff --git a/src/com/android/car/carlauncher/CarLauncher.java b/src/com/android/car/carlauncher/CarLauncher.java index 4eac517..3fa0cc4 100644 --- a/src/com/android/car/carlauncher/CarLauncher.java +++ b/src/com/android/car/carlauncher/CarLauncher.java @@ -1,23 +1,188 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.android.car.carlauncher; -import android.app.Activity; +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.ActivityView; +import android.app.UserSwitchObserver; import android.content.Intent; +import android.content.res.Configuration; import android.os.Bundle; -import android.view.View; +import android.os.IRemoteCallback; +import android.os.RemoteException; +import android.util.Log; +import android.widget.FrameLayout; + +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentTransaction; + +import com.android.car.media.common.PlaybackFragment; + +import java.util.Set; /** - * Basic Launcher for Android Automotive + * Basic Launcher for Android Automotive which demonstrates the use of {@link ActivityView} to host + * maps content. + * + * <p>Note: On some devices, the ActivityView may render with a width, height, and/or aspect + * ratio that does not meet Android compatibility definitions. Developers should work with content + * owners to ensure content renders correctly when extending or emulating this class. + * + * <p>Note: Since the hosted maps Activity in ActivityView is currently in a virtual display, the + * system considers the Activity to always be in front. Launching the maps Activity with a direct + * Intent will not work. To start the maps Activity on the real display, send the Intent to the + * Launcher with the {@link Intent#CATEGORY_APP_MAPS} category, and the launcher will start the + * Activity on the real display. + * + * <p>Note: The state of the virtual display in the ActivityView is nondeterministic when + * switching away from and back to the current user. To avoid a crash, this Activity will finish + * when switching users. */ -public class CarLauncher extends Activity { +public class CarLauncher extends FragmentActivity { + private static final String TAG = "CarLauncher"; + + private ActivityView mActivityView; + private boolean mActivityViewReady = false; + private boolean mIsStarted = false; + + private final ActivityView.StateCallback mActivityViewCallback = + new ActivityView.StateCallback() { + @Override + public void onActivityViewReady(ActivityView view) { + mActivityViewReady = true; + startMapsInActivityView(); + } + + @Override + public void onActivityViewDestroyed(ActivityView view) { + mActivityViewReady = false; + } - @Override + @Override + public void onTaskMovedToFront(int taskId) { + try { + if (mIsStarted) { + ActivityManager am = + (ActivityManager) getSystemService(ACTIVITY_SERVICE); + am.moveTaskToFront(CarLauncher.this.getTaskId(), /* flags= */ 0); + } + } catch (RuntimeException e) { + Log.w(TAG, "Failed to move CarLauncher to front."); + } + } + }; + + @Override protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.car_launcher); - } + super.onCreate(savedInstanceState); + // Don't show the maps panel in multi window mode. + // NOTE: CTS tests for split screen are not compatible with activity views on the default + // activity of the launcher + if (isInMultiWindowMode() || isInPictureInPictureMode()) { + setContentView(R.layout.car_launcher_multiwindow); + } else { + setContentView(R.layout.car_launcher); + } + initializeFragments(); + mActivityView = findViewById(R.id.maps); + if (mActivityView != null) { + mActivityView.setCallback(mActivityViewCallback); + } + } + + @Override + protected void onNewIntent(Intent intent) { + Set<String> categories = intent.getCategories(); + if (categories != null && categories.size() == 1 && categories.contains( + Intent.CATEGORY_APP_MAPS)) { + launchMapsActivity(); + } + } + + @Override + protected void onRestart() { + super.onRestart(); + startMapsInActivityView(); + } + + @Override + protected void onStart() { + super.onStart(); + mIsStarted = true; + } + + @Override + protected void onStop() { + super.onStop(); + mIsStarted = false; + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (mActivityView != null && mActivityViewReady) { + mActivityView.release(); + } + } + + private void startMapsInActivityView() { + // If we happen to be be resurfaced into a multi display mode we skip launching content + // in the activity view as we will get recreated anyway. + if (!mActivityViewReady || isInMultiWindowMode() || isInPictureInPictureMode()) { + return; + } + if (mActivityView != null) { + mActivityView.startActivity(getMapsIntent()); + } + } + + private void launchMapsActivity() { + // Make sure the Activity launches on the current display instead of in the ActivityView + // virtual display. + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(getDisplay().getDisplayId()); + startActivity(getMapsIntent(), options.toBundle()); + } + + private Intent getMapsIntent() { + return Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_MAPS); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + initializeFragments(); + } + + private void initializeFragments() { + PlaybackFragment playbackFragment = new PlaybackFragment(); + ContextualFragment contextualFragment = null; + FrameLayout contextual = findViewById(R.id.contextual); + if(contextual != null) { + contextualFragment = new ContextualFragment(); + } - // called by onClick in xml - public void openAppsList(View v){ - startActivity(new Intent(this, AppGridActivity.class)); + FragmentTransaction fragmentTransaction = + getSupportFragmentManager().beginTransaction(); + fragmentTransaction.replace(R.id.playback, playbackFragment); + if(contextual != null) { + fragmentTransaction.replace(R.id.contextual, contextualFragment); + } + fragmentTransaction.commitNow(); } } diff --git a/src/com/android/car/carlauncher/ContextualFragment.java b/src/com/android/car/carlauncher/ContextualFragment.java new file mode 100644 index 0000000..522a362 --- /dev/null +++ b/src/com/android/car/carlauncher/ContextualFragment.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.car.carlauncher; + +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProviders; + +/** {@link Fragment} which displays relevant information that changes over time. */ +public class ContextualFragment extends Fragment { + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.contextual_fragment, container, false); + ImageView iconView = rootView.findViewById(R.id.icon); + TextView topLineView = rootView.findViewById(R.id.top_line); + TextView bottomLineView = rootView.findViewById(R.id.bottom_line); + View dateDividerView = rootView.findViewById(R.id.date_divider); + View dateView = rootView.findViewById(R.id.date); + View bottomLineContainerView = rootView.findViewById(R.id.bottom_line_container); + + ContextualViewModel viewModel = ViewModelProviders.of(this).get(ContextualViewModel.class); + + viewModel.getContextualInfo().observe(this, info -> { + if (info == null) { + return; + } + + iconView.setImageDrawable(info.getIcon()); + topLineView.setText(info.getTopLine()); + + boolean showBottomLineMessage = (info.getBottomLine() != null); + + bottomLineView.setVisibility(showBottomLineMessage ? View.VISIBLE : View.GONE); + bottomLineView.setText(info.getBottomLine()); + + dateView.setVisibility(info.getShowClock() ? View.VISIBLE : View.GONE); + + // If both the bottom-line message and the clock are shown, show the divider. + dateDividerView.setVisibility( + (showBottomLineMessage && info.getShowClock()) ? View.VISIBLE : View.GONE); + // Hide the bottom-line container if neither the bottom-line message nor the clock + // is being shown. This will center the top-line message in the card. + bottomLineContainerView.setVisibility( + (showBottomLineMessage || info.getShowClock()) ? View.VISIBLE : View.GONE); + + Intent onClickActivity = info.getOnClickActivity(); + View.OnClickListener listener = + onClickActivity != null + ? v -> startActivity(info.getOnClickActivity()) + : null; + rootView.setOnClickListener(listener); + rootView.setClickable(listener != null); + }); + + return rootView; + } +} diff --git a/src/com/android/car/carlauncher/ContextualInfo.java b/src/com/android/car/carlauncher/ContextualInfo.java new file mode 100644 index 0000000..a85a53e --- /dev/null +++ b/src/com/android/car/carlauncher/ContextualInfo.java @@ -0,0 +1,62 @@ +package com.android.car.carlauncher; + +import android.content.Intent; +import android.graphics.drawable.Drawable; + +import androidx.annotation.Nullable; + +final class ContextualInfo { + private final Drawable mIcon; + private final CharSequence mTopLine; + private final @Nullable CharSequence mBottomLine; + private final boolean mShowClock; + private final Intent mOnClickActivity; + + public ContextualInfo( + Drawable icon, + CharSequence topLine, + @Nullable CharSequence bottomLine, + boolean showClock, + @Nullable Intent onClickActivity) { + mIcon = icon; + mTopLine = topLine; + mBottomLine = bottomLine; + mShowClock = showClock; + mOnClickActivity = onClickActivity; + } + + /** Gets the icon to be shown in the contextual space. */ + public Drawable getIcon() { + return mIcon; + } + + /** Gets the top line of the text to be shown in the contextual space. */ + public CharSequence getTopLine() { + return mTopLine; + } + + /** + * Gets the bottom line of the text to be shown in the contextual space. + * + * If null, no bottom-line text will be shown in the contextual space. + */ + @Nullable + public CharSequence getBottomLine() { + return mBottomLine; + } + + /** Gets whether to show the date in the contextual space. */ + public boolean getShowClock() { + return mShowClock; + } + + /** + * Gets the {@link Intent} for the activity to be started when the contextual space is tapped. + * + * If null, the contextual space will not be tappable. + */ + @Nullable + public Intent getOnClickActivity() { + return mOnClickActivity; + } +} diff --git a/src/com/android/car/carlauncher/ContextualViewModel.java b/src/com/android/car/carlauncher/ContextualViewModel.java new file mode 100644 index 0000000..b1538df --- /dev/null +++ b/src/com/android/car/carlauncher/ContextualViewModel.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.car.carlauncher; + +import android.app.Application; +import android.car.Car; +import android.car.CarNotConnectedException; +import android.car.CarProjectionManager; +import android.car.CarProjectionManager.ProjectionStatusListener; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.UserManager; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MediatorLiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; +import androidx.lifecycle.Transformations; +import androidx.lifecycle.ViewModel; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Implementation {@link ViewModel} for {@link ContextualFragment}. + * + * Returns the first non-null {@link ContextualInfo} from a set of delegates. + */ +public class ContextualViewModel extends AndroidViewModel { + private final MediatorLiveData<ContextualInfo> mContextualInfo = new MediatorLiveData<>(); + + private final List<LiveData<ContextualInfo>> mInfoDelegates; + + public ContextualViewModel(Application application) { + this(application, getCarProjectionManager(application)); + } + + private static CarProjectionManager getCarProjectionManager(Context context) { + return (CarProjectionManager) + Car.createCar(context).getCarManager(Car.PROJECTION_SERVICE); + } + + @VisibleForTesting + ContextualViewModel(Application application, CarProjectionManager carProjectionManager) { + super(application); + + + mInfoDelegates = + Collections.unmodifiableList(Arrays.asList( + new ProjectionContextualInfoLiveData(application, carProjectionManager), + new WeatherContextualInfoLiveData(application) + )); + + Observer<Object> observer = x -> updateLiveData(); + for (LiveData<ContextualInfo> delegate : mInfoDelegates) { + mContextualInfo.addSource(delegate, observer); + } + } + + private void updateLiveData() { + for (LiveData<ContextualInfo> delegate : mInfoDelegates) { + ContextualInfo value = delegate.getValue(); + if (value != null) { + mContextualInfo.setValue(value); + return; + } + } + + mContextualInfo.setValue(null); + } + + public LiveData<ContextualInfo> getContextualInfo() { + return mContextualInfo; + } +} diff --git a/src/com/android/car/carlauncher/LocalizedTextClock.java b/src/com/android/car/carlauncher/LocalizedTextClock.java new file mode 100644 index 0000000..49b9666 --- /dev/null +++ b/src/com/android/car/carlauncher/LocalizedTextClock.java @@ -0,0 +1,43 @@ +package com.android.car.carlauncher; + +import android.content.Context; +import android.text.format.DateFormat; +import android.util.AttributeSet; +import android.widget.TextClock; + +import java.util.Locale; + +/** + * {@link TextClock} implementation which expects a date format skeleton for + * {@link android.R.styleable#TextClock_format12Hour} and + * {@link android.R.styleable#TextClock_format24Hour} and applies the best format as determined by + * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. + */ +public class LocalizedTextClock extends TextClock { + + public LocalizedTextClock(Context context) { + super(context); + } + + public LocalizedTextClock(Context context, AttributeSet attrs) { + super(context, attrs, 0); + } + + public LocalizedTextClock(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public LocalizedTextClock(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + setFormat12Hour(DateFormat.getBestDateTimePattern(Locale.getDefault(), + getFormat12Hour().toString())); + setFormat24Hour(DateFormat.getBestDateTimePattern(Locale.getDefault(), + getFormat24Hour().toString())); + } +} diff --git a/src/com/android/car/carlauncher/ProjectionContextualInfoLiveData.java b/src/com/android/car/carlauncher/ProjectionContextualInfoLiveData.java new file mode 100644 index 0000000..bd3d466 --- /dev/null +++ b/src/com/android/car/carlauncher/ProjectionContextualInfoLiveData.java @@ -0,0 +1,123 @@ +package com.android.car.carlauncher; + +import android.car.CarProjectionManager; +import android.car.projection.ProjectionStatus; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; + +import java.util.List; + +/** A {@link LiveData} of {@link ContextualInfo} on projection status. */ +class ProjectionContextualInfoLiveData extends LiveData<ContextualInfo> + implements CarProjectionManager.ProjectionStatusListener { + private static final String TAG = "ProjectionContext"; + + private final Context mContext; + private final CarProjectionManager mCarProjectionManager; + + ProjectionContextualInfoLiveData( + Context context, + CarProjectionManager carProjectionManager) { + mContext = context; + mCarProjectionManager = carProjectionManager; + } + + @Override + protected void onActive() { + super.onActive(); + mCarProjectionManager.registerProjectionStatusListener(this); + } + + @Override + protected void onInactive() { + mCarProjectionManager.unregisterProjectionStatusListener(this); + super.onInactive(); + } + + @Override + public void onProjectionStatusChanged( + int state, @Nullable String packageName, @NonNull List<ProjectionStatus> details) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "onProjectionStatusChanged state=" + state + " package=" + packageName); + } + if (state == ProjectionStatus.PROJECTION_STATE_INACTIVE || packageName == null) { + setValue(null); + return; + } + + PackageManager pm = mContext.getPackageManager(); + ApplicationInfo applicationInfo; + try { + applicationInfo = pm.getApplicationInfo(packageName, 0); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Could not load projection package information", e); + setValue(null); + return; + } + + setValue( + new ContextualInfo( + applicationInfo.loadIcon(pm), + applicationInfo.loadLabel(pm), + getStatusMessage(packageName, details), + /* showClock= */ false, + pm.getLaunchIntentForPackage(packageName))); + } + + @Nullable + private String getStatusMessage( + String packageName, List<ProjectionStatus> details) { + for (ProjectionStatus status : details) { + if (packageName.equals(status.getPackageName())) { + return getStatusMessage(status); + } + } + + return null; + } + + @Nullable + private String getStatusMessage(ProjectionStatus status) { + // The status message is as follows: + // - If there is an unambiguous "best" device, the name of that device. + // "Unambiguous" is defined as only one projecting device, or no projecting devices + // and only one non-projecting device. + // - If there are multiple projecting or non-projecting devices, "N devices", where N + // is the total number of projecting and non-projecting devices. + // - If there are no devices at all, no message. This should not happen if projection + // apps are behaving properly, but may happen in the event of a projection app bug. + String projectingDevice = null; + String nonProjectingDevice = null; + int projectingDeviceCount = 0; + int nonProjectingDeviceCount = 0; + for (ProjectionStatus.MobileDevice device : status.getConnectedMobileDevices()) { + if (device.isProjecting()) { + projectingDevice = device.getName(); + projectingDeviceCount++; + } else { + nonProjectingDevice = device.getName(); + nonProjectingDeviceCount++; + } + } + + if (projectingDeviceCount == 1) { + return projectingDevice; + } else if (projectingDeviceCount == 0 && nonProjectingDeviceCount == 1) { + return nonProjectingDevice; + } + + int totalDeviceCount = projectingDeviceCount + nonProjectingDeviceCount; + if (totalDeviceCount > 0) { + return mContext.getResources().getQuantityString( + R.plurals.projection_devices, totalDeviceCount, totalDeviceCount); + } else { + return null; + } + } +} diff --git a/src/com/android/car/carlauncher/RecentAppsRowViewHolder.java b/src/com/android/car/carlauncher/RecentAppsRowViewHolder.java index 8090fe5..f0351e1 100644 --- a/src/com/android/car/carlauncher/RecentAppsRowViewHolder.java +++ b/src/com/android/car/carlauncher/RecentAppsRowViewHolder.java @@ -21,9 +21,7 @@ import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; import android.widget.LinearLayout; -import android.widget.TextView; import androidx.recyclerview.widget.RecyclerView; @@ -62,18 +60,9 @@ public class RecentAppsRowViewHolder extends RecyclerView.ViewHolder { for (int i = 0; i < size; i++) { View view = LayoutInflater.from(mContext).inflate(R.layout.app_item, mRecentAppsRow, false); - ImageView iconView = view.findViewById(R.id.app_icon); - TextView appNameView = view.findViewById(R.id.app_name); - AppMetaData app = apps.get(i); - - if (isDistractionOptimizationRequired && !app.getIsDistractionOptimized()) { - iconView.setImageDrawable(AppLauncherUtils.toGrayscale(app.getIcon())); - } else { - iconView.setImageDrawable(app.getIcon()); - view.setOnClickListener(v -> AppLauncherUtils.launchApp(mContext, app)); - } - appNameView.setText(app.getDisplayName()); + AppItemViewHolder holder = new AppItemViewHolder(view, mContext); + holder.bind(apps.get(i), isDistractionOptimizationRequired); LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) view.getLayoutParams(); diff --git a/src/com/android/car/carlauncher/WeatherContextualInfoLiveData.java b/src/com/android/car/carlauncher/WeatherContextualInfoLiveData.java new file mode 100644 index 0000000..96c9773 --- /dev/null +++ b/src/com/android/car/carlauncher/WeatherContextualInfoLiveData.java @@ -0,0 +1,47 @@ +package com.android.car.carlauncher; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.UserManager; + +import androidx.lifecycle.LiveData; + +/** A {@link LiveData} that returns placeholder weather {@link ContextualInfo}. */ +class WeatherContextualInfoLiveData extends LiveData<ContextualInfo> { + private final Context mContext; + + WeatherContextualInfoLiveData(Context context) { + mContext = context; + } + + @Override + protected void onActive() { + super.onActive(); + setValue( + new ContextualInfo( + getWeatherIcon(), + getGreeting(), + getTemperature(), + /* showClock= */ true, + /* onClickActivity= */ null)); + } + + private Drawable getWeatherIcon() { + return mContext.getDrawable(R.drawable.ic_partly_cloudy); + } + + private CharSequence getGreeting() { + UserManager userManager = UserManager.get(mContext); + String userName = userManager.getUserName(); + + if (userName != null) { + return mContext.getString(R.string.greeting, userName); + } else { + return ""; + } + } + + private CharSequence getTemperature() { + return mContext.getText(R.string.temperature_empty); + } +} |
