diff options
Diffstat (limited to 'Host/app/renderer/src/main/java/com/android/car/libraries')
267 files changed, 26319 insertions, 0 deletions
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/BootCompleteReceiver.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/BootCompleteReceiver.kt new file mode 100644 index 0000000..1df079e --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/BootCompleteReceiver.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import com.android.car.libraries.apphost.logging.L +import com.android.car.libraries.apphost.logging.LogTags + +/** A [BroadcastReceiver] used to pre-warm the host and set it as a foreground service. */ +class BootCompleteReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + L.d(LogTags.SERVICE) { "StartUpBootReceiver: received ${intent.action}" } + val serviceIntent = Intent(context, RendererService::class.java) + context.startForegroundService(serviceIntent) + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/RendererService.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/RendererService.kt new file mode 100644 index 0000000..61c3935 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/RendererService.kt @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION +import android.content.res.Configuration +import android.os.Binder +import android.os.IBinder +import android.view.LayoutInflater +import androidx.car.app.HandshakeInfo +import androidx.car.app.activity.renderer.ICarAppActivity +import androidx.car.app.activity.renderer.IRendererService +import androidx.car.app.serialization.Bundleable +import androidx.car.app.versioning.CarAppApiLevels +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.android.car.libraries.apphost.common.HostResourceIds +import com.android.car.libraries.apphost.common.ThreadUtils +import com.android.car.libraries.apphost.logging.L +import com.android.car.libraries.apphost.logging.LogTags +import com.android.car.libraries.apphost.logging.StatusReporter +import com.android.car.libraries.apphost.view.TemplateConverterRegistry +import com.android.car.libraries.apphost.view.TemplatePresenterRegistry +import com.android.car.libraries.templates.host.di.FeaturesConfig +import com.android.car.libraries.templates.host.di.HostApiLevelConfig +import com.android.car.libraries.templates.host.di.TelemetryHandlerFactory +import com.android.car.libraries.templates.host.di.ThemeManager +import com.android.car.libraries.templates.host.di.UxreConfig +import com.android.car.libraries.templates.host.internal.CarActivityDispatcher +import com.android.car.libraries.templates.host.internal.CommonUtils +import com.android.car.libraries.templates.host.internal.LogUtil +import com.android.car.libraries.templates.host.internal.StatusManager +import com.android.car.libraries.templates.host.internal.debug.ClusterActivity +import com.android.car.libraries.templates.host.renderer.ScreenRenderer +import com.android.car.libraries.templates.host.renderer.ScreenRendererRepository +import com.android.car.libraries.templates.host.view.presenters.common.CommonTemplateConverter +import com.android.car.libraries.templates.host.view.presenters.common.CommonTemplatePresenterFactory +import com.android.car.libraries.templates.host.view.presenters.maps.MapsTemplatePresenterFactory +import com.android.car.libraries.templates.host.view.presenters.navigation.NavigationTemplatePresenterFactory +import com.android.car.ui.CarUiLayoutInflaterFactory +import dagger.hilt.android.AndroidEntryPoint +import java.io.FileDescriptor +import java.io.PrintWriter +import javax.inject.Inject + +/** A service used to render content of a car app service inside a car app activity. */ +@AndroidEntryPoint +class RendererService : Service() { + // TODO(b/182486338): Migrate the inject point to TemplateView + @Inject lateinit var mapsTemplatePresenterFactory: MapsTemplatePresenterFactory + + @Inject lateinit var hostResourceIds: HostResourceIds + + @Inject lateinit var uxreConfig: UxreConfig + + @Inject lateinit var hostApiLevelConfig: HostApiLevelConfig + + @Inject lateinit var themeManager: ThemeManager + + @Inject lateinit var telemetryHandlerFactory: TelemetryHandlerFactory + @Inject lateinit var hostFeaturesConfig: FeaturesConfig + + /** Whether the debug overlay is active. */ + private var isDebugOverlayActive = false + + override fun onCreate() { + super.onCreate() + L.d(LogTags.SERVICE) { "RendererService.onCreate" } + + // This must be executed within ANR timeout (5 seconds) of the host being launched. + setAsForeground() + LogUtil.init(telemetryHandlerFactory, applicationContext) + + val layoutInflater = LayoutInflater.from(this.applicationContext) + if (layoutInflater.factory2 == null) { + layoutInflater.factory2 = CarUiLayoutInflaterFactory() + } + + // preload some MapView rendering code to speed things up before it is actually used + // by PlaceListMapTemplatePresenter. + ThreadUtils.enqueueOnMain { mapsTemplatePresenterFactory.preloadMapView(this) } + initClusterActivity() + } + + private fun initClusterActivity() { + val state = + if (hostFeaturesConfig.isClusterActivityEnabled()) { + PackageManager.COMPONENT_ENABLED_STATE_ENABLED + } else { + PackageManager.COMPONENT_ENABLED_STATE_DISABLED + } + packageManager.setComponentEnabledSetting( + ComponentName(this, ClusterActivity::class.java), + state, + PackageManager.DONT_KILL_APP + ) + } + + private fun setAsForeground() { + val channel = + NotificationChannel( + CHANNEL_ID, + application.applicationInfo.name, + NotificationManager.IMPORTANCE_NONE + ) + val notificationManager = NotificationManagerCompat.from(this) + notificationManager.createNotificationChannel(channel) + + val notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID) + val notification = + notificationBuilder + .setOngoing(true) + .setSmallIcon(application.applicationInfo.icon) + .setPriority(NotificationCompat.PRIORITY_MIN) + .setCategory(Notification.CATEGORY_SERVICE) + .build() + + startForeground( + FOREGROUND_SERVICE_NOTIFICATION_ID, + notification, + FOREGROUND_SERVICE_TYPE_LOCATION + ) + } + + override fun onBind(intent: Intent): IBinder { + registerPresenters() + return RendererServiceBinder(this) + } + + override fun onUnbind(intent: Intent): Boolean { + L.d(LogTags.SERVICE) { "RendererService.onUnbind" } + + // Note that even when the RendererService is unbound. The CarAppService remains bound + // because the car app can remain alive in the background (e.g. nav apps sending TBT + // instructions). Hence we do not clear the Carhosts here. + ScreenRendererRepository.clear() + return super.onUnbind(intent) + } + + override fun onDestroy() { + L.d(LogTags.SERVICE) { "RendererService.onDestroy" } + + // Note that even when the RendererService is unbound. The CarAppService remains bound + // because the car app can remain alive in the background (e.g. nav apps sending TBT + // instructions). Hence we do not clear the Carhosts here. + ScreenRendererRepository.clear() + super.onDestroy() + } + + override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array<out String>?) { + if (args?.contains("debug_overlay") == true) { + if (!CommonUtils.isDebugEnabled(/* context= */ this)) { + writer?.println("Debug enabled required for debug overlay") + return + } + isDebugOverlayActive = !isDebugOverlayActive + ScreenRendererRepository.getAll().forEach { it.showDebugOverlay(isDebugOverlayActive) } + if (isDebugOverlayActive) { + writer?.println("Debug overlay enabled") + } else { + writer?.println("Debug overlay disabled") + } + } else { + writer?.let { StatusManager.reportStatus(writer, StatusReporter.Pii.HIDE) } + } + } + + override fun onConfigurationChanged(config: Configuration) { + super.onConfigurationChanged(config) + + ScreenRendererRepository.getAll().forEach { it.onConfigurationChanged(config) } + } + + private fun registerPresenters() { + TemplatePresenterRegistry.get().clear() + TemplatePresenterRegistry.get().register(NavigationTemplatePresenterFactory.get()) + TemplatePresenterRegistry.get().register(CommonTemplatePresenterFactory.get()) + TemplateConverterRegistry.get().register(CommonTemplateConverter.get()) + TemplatePresenterRegistry.get().register(mapsTemplatePresenterFactory) + } + + private inner class RendererServiceBinder(val context: Context) : + IRendererService.Stub(), CarActivityDispatcher.Callback { + + override fun initialize( + carActivity: ICarAppActivity, + serviceName: ComponentName, + displayId: Int + ): Boolean { + L.d(LogTags.SERVICE) { "RendererServiceBinder.initialize: $serviceName" } + val renderer = findRenderer(serviceName, displayId) ?: return false + ThreadUtils.runOnMain { renderer.onCreateActivity(carActivity) } + return true + } + + override fun terminate(serviceName: ComponentName) { + if (!isValid(serviceName)) return + L.d(LogTags.SERVICE) { "RendererServiceBinder.terminate: $serviceName" } + doTerminate(serviceName) + } + + override fun onDisconnect(serviceName: ComponentName) { + L.d(LogTags.SERVICE) { "RendererServiceBinder.onDisconnect: $serviceName" } + doTerminate(serviceName) + } + + private fun doTerminate(serviceName: ComponentName) { + ThreadUtils.runOnMain { ScreenRendererRepository.remove(serviceName)?.onDestroy() } + } + + override fun onNewIntent(intent: Intent, serviceName: ComponentName, displayId: Int): Boolean { + L.i(LogTags.SERVICE) { "RendererServiceBinder.onNewIntent: $serviceName" } + val renderer = findRenderer(serviceName, displayId) ?: return false + ThreadUtils.runOnMain { renderer.onNewIntent(intent) } + return true + } + + override fun performHandshake(serviceName: ComponentName, appLatestApiLevel: Int): Bundleable { + val apiLevel = Math.min(appLatestApiLevel, CarAppApiLevels.getLatest()) + L.i(LogTags.SERVICE) { + "RendererServiceBinder.performHandshake: $serviceName, " + + "appLatestApiLevel: $appLatestApiLevel, chosen api level: $apiLevel" + } + // Store in the host whenever we need to start checking versions. + return Bundleable.create(HandshakeInfo(context.packageName, apiLevel)) + } + + private fun findRenderer(serviceName: ComponentName, displayId: Int): ScreenRenderer? { + if (!isValid(serviceName)) return null + + return ScreenRendererRepository.computeIfAbsent(serviceName) { + L.d(LogTags.SERVICE) { + "RendererServiceBinder.findRenderer: $serviceName - " + "created new ScreenRenderer" + } + ScreenRenderer( + context.applicationContext, + serviceName, + displayId, + this, + hostResourceIds, + uxreConfig, + hostApiLevelConfig, + themeManager, + telemetryHandlerFactory.create(context, serviceName), + hostFeaturesConfig, + isDebugOverlayActive + ) + } + } + + private fun isValid(serviceName: ComponentName?): Boolean { + if (serviceName == null) { + L.e(LogTags.SERVICE) { "Service name was not specified!" } + return false + } + val senderPackage = context.packageManager.getNameForUid(Binder.getCallingUid()) + if (senderPackage == null || senderPackage != serviceName.packageName) { + L.e(LogTags.SERVICE) { "Could not verify the caller!" } + return false + } + return true + } + } + + companion object { + const val CHANNEL_ID = "default" + const val FOREGROUND_SERVICE_NOTIFICATION_ID = 1 + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/FeaturesConfig.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/FeaturesConfig.java new file mode 100644 index 0000000..5bb5d9a --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/FeaturesConfig.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.di; +/** An interface for providing host features config */ +public interface FeaturesConfig { + /** Returns whether the host has cluster activity features enabled */ + boolean isClusterActivityEnabled(); + + /** Returns whether the host supports pan and zoom features in the navigation template */ + boolean isNavPanZoomEnabled(); + + /** Returns whether the host supports pan and zoom features in POI and route preview templates */ + boolean isPoiRoutePreviewPanZoomEnabled(); + + /** Returns whether the host supports content refresh on POI templates */ + boolean isPoiContentRefreshEnabled(); +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/HostApiLevelConfig.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/HostApiLevelConfig.java new file mode 100644 index 0000000..80aea9d --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/HostApiLevelConfig.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.di; + +import android.content.ComponentName; + +/** An interface for providing host API overrides */ +public interface HostApiLevelConfig { + /** The min api level for given car app */ + int getHostMinApiLevel(int defaultValue, ComponentName componentName); + + /** The max api level for given car app */ + int getHostMaxApiLevel(int defaultValue, ComponentName componentName); +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/MapViewContainerFactory.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/MapViewContainerFactory.java new file mode 100644 index 0000000..84f5c91 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/MapViewContainerFactory.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.di; + +import android.content.Context; +import com.android.car.libraries.apphost.view.widget.map.AbstractMapViewContainer; + +/** An interface used for creating a {@link AbstractMapViewContainer} */ +public interface MapViewContainerFactory { + + /** returns a AbstractMapViewContainer that used in {@link AbstractMapViewContainer} */ + AbstractMapViewContainer create(Context context, int theme); +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/TelemetryHandlerFactory.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/TelemetryHandlerFactory.java new file mode 100644 index 0000000..f52df92 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/TelemetryHandlerFactory.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.di; + +import android.content.ComponentName; +import android.content.Context; +import com.android.car.libraries.apphost.logging.TelemetryHandler; + +/** An interface used for creating a {@link TelemetryHandler} */ +public interface TelemetryHandlerFactory { + + /** Returns a new {@link TelemetryHandler} instance */ + TelemetryHandler create(Context context, ComponentName componentName); +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/ThemeManager.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/ThemeManager.java new file mode 100644 index 0000000..beca618 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/ThemeManager.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.di; + +import android.content.Context; + +/** Provides the theme that should be used throughout the UI. */ +public interface ThemeManager { + + /** Applies appropriate theme to the context. */ + void applyTheme(Context context); +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/UxreConfig.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/UxreConfig.java new file mode 100644 index 0000000..5c86c4c --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/UxreConfig.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.di; + +/** An interface used for providing UXRE configs */ +public interface UxreConfig { + + /** The max size of a car app template stack */ + int getTemplateStackMaxSize(int defaultValue); + + /** The max length of a car app list for showing routes. */ + int getRouteListMaxLength(int defaultValue); + + /** The max length of a car app list for showing pane information. */ + int getPaneMaxLength(int defaultValue); + + /** The max length of a car app grid view. */ + int getGridMaxLength(int defaultValue); + + /** + * The max length of a generic, uniform car app list for cases where the OEM did not override the + * default UXRE cumulative content limit value. + */ + int getListMaxLength(int defaultValue); + + /** Default max string length */ + int getCarAppDefaultMaxStringLength(int defaultValue); +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/AppIconLoaderImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/AppIconLoaderImpl.kt new file mode 100644 index 0000000..8ba55af --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/AppIconLoaderImpl.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import android.content.ComponentName +import android.content.Context +import android.graphics.drawable.Drawable +import com.android.car.libraries.apphost.common.AppIconLoader + +/** Android Automotive implementation of [AppIconLoader] */ +object AppIconLoaderImpl : AppIconLoader { + override fun getRoundAppIcon(context: Context, componentName: ComponentName): Drawable { + return try { + val pm = context.packageManager + val applicationInfo = pm.getApplicationInfo(componentName.packageName, 0) + val appIconResId = applicationInfo.icon + + pm.getResourcesForApplication(componentName.packageName).getDrawable(appIconResId, null) + } catch (ex: Exception) { + getDefaultAppIcon(context) + } + } + + private fun getDefaultAppIcon(context: Context): Drawable { + return context.resources.getDrawable(android.R.drawable.sym_def_app_icon, null) + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarActivityDispatcher.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarActivityDispatcher.java new file mode 100644 index 0000000..fdff26b --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarActivityDispatcher.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal; + +import android.content.ComponentName; +import android.os.DeadObjectException; +import android.os.RemoteException; +import android.util.Log; +import androidx.car.app.activity.renderer.ICarAppActivity; +import com.android.car.libraries.apphost.logging.LogTags; + +/** + * A dispatcher that can be used to send messages to {@link ICarAppActivity}, handling any remote + * errors. + */ +public class CarActivityDispatcher { + private final ComponentName mAppName; + private final ICarAppActivity mCarAppActivity; + private final Callback mCallback; + private boolean mIsConnected; + + public CarActivityDispatcher( + ComponentName appName, ICarAppActivity carActivity, Callback callback) { + mAppName = appName; + mCarAppActivity = carActivity; + mCallback = callback; + mIsConnected = true; + } + + /** {@link CarActivityDispatcher} callbacks */ + public interface Callback { + /** Notifies that the client associated with this {@link ICarAppActivity} is disconnected */ + void onDisconnect(ComponentName appName); + } + + /** An IPC call that can be dispatched by this dispatcher */ + public interface IPCCall { + /** Remote invocation to execute */ + void call(ICarAppActivity carActivity) throws RemoteException; + } + + /** Returns true if the application is still considered to be connected */ + public boolean isConnected() { + return mIsConnected; + } + + /** + * Dispatches an IPC call to the {@link ICarAppActivity} associated with this dispatcher. If this + * result in an error, the dispatcher will handle the error and returns false. + * + * @return true iif dispatch is successful. + */ + public boolean dispatchNoFail(IPCCall call) { + if (!mIsConnected) { + // Ignoring request as we have already disconnected from the client app + return false; + } + + try { + call.call(mCarAppActivity); + return true; + } catch (DeadObjectException e) { + Log.w(LogTags.APP_HOST, "App " + mAppName + " is dead", e); + return false; + } catch (RemoteException e) { + Log.w(LogTags.APP_HOST, "App " + mAppName + " has caused a remote exception", e); + return false; + } catch (Throwable e) { + Log.w(LogTags.APP_HOST, "App " + mAppName + " caused an unknown error", e); + return false; + } + } + + /** + * Dispatches an IPC call to the {@link ICarAppActivity} associated with this dispatcher. If this + * result in an error, the dispatcher will handle the error and then call {@link + * Callback#onDisconnect(ComponentName)} to notify that this client is not longer valid. + */ + public void dispatch(IPCCall call) { + if (!mIsConnected) { + // Ignoring request as we have already disconnected from the client app + return; + } + + try { + call.call(mCarAppActivity); + } catch (DeadObjectException e) { + Log.e(LogTags.APP_HOST, "App " + mAppName + " is dead", e); + mIsConnected = false; + mCallback.onDisconnect(mAppName); + } catch (RemoteException e) { + Log.e(LogTags.APP_HOST, "App " + mAppName + " has caused a remote exception", e); + disconnect(); + } catch (Throwable e) { + Log.e(LogTags.APP_HOST, "App " + mAppName + " caused an unknown error", e); + disconnect(); + } + } + + /** Disconnects this dispatcher from its associated {@link ICarAppActivity} */ + public void disconnect() { + mIsConnected = false; + try { + mCarAppActivity.finishCarApp(); + } catch (Throwable e) { + // Ignoring error as we are already finishing anyways (avoid spamming the logs). + } + mCallback.onDisconnect(mAppName); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarAppServiceInfo.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarAppServiceInfo.java new file mode 100644 index 0000000..6ea5d12 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarAppServiceInfo.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal; + +import static android.content.pm.PackageManager.GET_RESOLVED_FILTER; +import static androidx.car.app.CarAppService.CATEGORY_NAVIGATION_APP; +import static androidx.car.app.CarAppService.SERVICE_INTERFACE; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; + +/** This class provides information about car app services. */ +public class CarAppServiceInfo { + private final PackageManager mPackageManager; + private final ComponentName mServiceName; + + public CarAppServiceInfo(Context context, ComponentName serviceName) { + mPackageManager = context.getPackageManager(); + mServiceName = serviceName; + } + + /** Returns true for navigation services. */ + public boolean isNavigationService() { + Intent intent = + new Intent(SERVICE_INTERFACE) + .setPackage(mServiceName.getPackageName()) + .addCategory(CATEGORY_NAVIGATION_APP); + return !mPackageManager.queryIntentServices(intent, GET_RESOLVED_FILTER).isEmpty(); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarHostConfigImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarHostConfigImpl.kt new file mode 100644 index 0000000..bca23e9 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarHostConfigImpl.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import androidx.annotation.StyleableRes +import androidx.car.app.versioning.CarAppApiLevels +import com.android.car.libraries.apphost.common.CarHostConfig +import com.android.car.libraries.templates.host.R +import com.android.car.libraries.templates.host.di.FeaturesConfig +import com.android.car.libraries.templates.host.di.HostApiLevelConfig + +/** Configuration options from the car host. */ +class CarHostConfigImpl( + private val context: Context, + appName: ComponentName, + hostApiLevelConfig: HostApiLevelConfig, + private val featuresConfig: FeaturesConfig +) : CarHostConfig(appName) { + private val hostMinApi: Int = + hostApiLevelConfig.getHostMinApiLevel(CarAppApiLevels.getOldest(), appName) + private val hostMaxApi: Int = + hostApiLevelConfig.getHostMaxApiLevel(CarAppApiLevels.getLatest(), appName) + + override fun getHostMinApi(): Int { + return hostMinApi + } + + override fun getHostMaxApi(): Int { + return hostMaxApi + } + + override fun isButtonColorOverriddenByOEM(): Boolean { + return getBooleanAttr(R.attr.templateActionButtonUseOemColors) + } + + override fun getAppUnbindSeconds(): Int { + return context.resources.getInteger( + R.integer.app_unbind_delay_seconds + ) + } + + override fun getHostIntentExtrasToRemove(): MutableList<String> { + return mutableListOf() + } + + override fun isNewTaskFlowIntent(intent: Intent?): Boolean { + return true + } + + override fun getPrimaryActionOrder(): Int { + return getIntAttr(R.attr.templateActionButtonPrimaryHorizontalOrder) + } + + override fun isClusterEnabled(): Boolean { + return featuresConfig.isClusterActivityEnabled() + } + + override fun isNavPanZoomEnabled(): Boolean { + return featuresConfig.isNavPanZoomEnabled() + } + + override fun isPoiRoutePreviewPanZoomEnabled(): Boolean { + return featuresConfig.isPoiRoutePreviewPanZoomEnabled() + } + + override fun isPoiContentRefreshEnabled(): Boolean { + return featuresConfig.isPoiContentRefreshEnabled() + } + + private fun getBooleanAttr(attr: Int): Boolean { + @StyleableRes val themeAttrs = intArrayOf(attr) + val ta = context.obtainStyledAttributes(themeAttrs) + val value = ta.getBoolean(0, false) + ta.recycle() + return value + } + + private fun getIntAttr(attr: Int): Int { + @StyleableRes val themeAttrs = intArrayOf(attr) + val ta = context.obtainStyledAttributes(themeAttrs) + val value = ta.getInt(0, 0) + ta.recycle() + return value + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarHostRepository.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarHostRepository.kt new file mode 100644 index 0000000..bc3138f --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarHostRepository.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import android.content.ComponentName +import androidx.annotation.MainThread +import com.android.car.libraries.apphost.CarHost +import com.android.car.libraries.apphost.logging.L +import com.android.car.libraries.apphost.logging.LogTags +import com.android.car.libraries.apphost.logging.StatusReporter +import com.google.common.base.Supplier +import java.io.PrintWriter +import java.util.concurrent.ConcurrentHashMap + +/** Manages a cache of [CarHost]s. */ +@MainThread +object CarHostRepository : StatusReporter { + private val cache: MutableMap<ComponentName, CarHost> = ConcurrentHashMap() + + init { + StatusManager.addStatusReporter(StatusManager.ReportSection.APP_HOST, this) + } + + /** + * Returns a [CarHost] for the given `appName` to use for app communication. If the key is not + * present in the cache, uses the [Supplier] to retrieve the [CarHost] and puts it in the cache. + */ + @Synchronized + fun computeIfAbsent(appName: ComponentName, carHostSupplier: Supplier<CarHost>): CarHost { + return cache.computeIfAbsent(appName) { carHostSupplier.get() } + } + + /** + * @return a [CarHost] for the given [appName] to use for app communication, or `null` if the key + * is not present in the cache. + */ + @Synchronized + fun get(appName: ComponentName): CarHost? { + return cache[appName] + } + + /** Invalidates and removes the [CarHost] from cache for the given [appName]. */ + @Synchronized + fun remove(appName: ComponentName) { + val carHost = cache.remove(appName) + carHost?.unbindFromApp() + carHost?.invalidate() + } + + /** Invalidates all the [CarHost] objects in the cache and empties the cache. */ + @Synchronized + fun clear() { + if (cache.isNotEmpty()) { + for (carHost in cache.values) { + carHost.unbindFromApp() + carHost.invalidate() + } + } + cache.clear() + } + + override fun reportStatus(pw: PrintWriter, piiHandling: StatusReporter.Pii) { + try { + pw.println("Car host cache") + pw.printf("- size: %d\n", cache.size) + pw.printf("- hosts: %d\n", cache.size) + for ((name, value) in cache) { + pw.println("\n-------------------------------") + pw.printf("Host: %s\n", name.flattenToShortString()) + value.reportStatus(pw, piiHandling) + } + } catch (t: Throwable) { + L.e(LogTags.APP_HOST, t, "Failed to produce status report for car host cache") + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarUxRestrictionsUtil.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarUxRestrictionsUtil.java new file mode 100644 index 0000000..2441bb5 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarUxRestrictionsUtil.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal; + +import android.car.Car; +import android.car.drivingstate.CarUxRestrictions; +import android.car.drivingstate.CarUxRestrictions.CarUxRestrictionsInfo; +import android.car.drivingstate.CarUxRestrictionsManager; +import android.content.Context; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import com.android.car.libraries.apphost.common.ThreadUtils; +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; + +/** + * Utility class to access Car Restriction Manager. + * + * <p>This class must be a singleton because only one listener can be registered with {@link + * CarUxRestrictionsManager} at a time, as documented in {@link + * CarUxRestrictionsManager#registerListener}. + */ +// TODO(b/187312393): Rename to CarUxRestrictionsManager +public class CarUxRestrictionsUtil { + private static final String TAG = "CarUxRestrictionsUtil"; + + @NonNull private CarUxRestrictions mCarUxRestrictions = getDefaultRestrictions(); + + private final Set<OnUxRestrictionsChangedListener> mObservers = + Collections.newSetFromMap(new WeakHashMap<>()); + + private static CarUxRestrictionsUtil sInstance = null; + + private CarUxRestrictionsUtil(Context context) { + CarUxRestrictionsManager.OnUxRestrictionsChangedListener listener = + (carUxRestrictions) -> { + if (carUxRestrictions == null) { + mCarUxRestrictions = getDefaultRestrictions(); + } else { + mCarUxRestrictions = carUxRestrictions; + } + + ThreadUtils.runOnMain( + () -> { + for (OnUxRestrictionsChangedListener observer : mObservers) { + observer.onRestrictionsChanged(mCarUxRestrictions); + } + }); + }; + + try { + Car carApi = Car.createCar(context.getApplicationContext()); + + try { + CarUxRestrictionsManager carUxRestrictionsManager = + (CarUxRestrictionsManager) carApi.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE); + carUxRestrictionsManager.registerListener(listener); + listener.onUxRestrictionsChanged(carUxRestrictionsManager.getCurrentCarUxRestrictions()); + } catch (NullPointerException e) { + Log.e(TAG, "Car not connected", e); + // mCarUxRestrictions will be the default + } + } catch (SecurityException e) { + Log.w(TAG, "Unable to connect to car service, assuming unrestricted", e); + listener.onUxRestrictionsChanged( + new CarUxRestrictions.Builder(false, CarUxRestrictions.UX_RESTRICTIONS_BASELINE, 0) + .build()); + } + } + + @NonNull + private static CarUxRestrictions getDefaultRestrictions() { + return new CarUxRestrictions.Builder( + true, CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED, 0) + .build(); + } + + /** Listener interface used to update clients on UxRestrictions changes */ + public interface OnUxRestrictionsChangedListener { + /** Called when CarUxRestrictions changes */ + void onRestrictionsChanged(@NonNull CarUxRestrictions carUxRestrictions); + } + + /** Returns the singleton sInstance of this class */ + @NonNull + public static CarUxRestrictionsUtil getInstance(Context context) { + if (sInstance == null) { + sInstance = new CarUxRestrictionsUtil(context); + } + + return sInstance; + } + + /** + * Registers a listener on this class for updates to CarUxRestrictions. Multiple listeners may be + * registered. Note that this class will only hold a weak reference to the listener, you must + * maintain a strong reference to it elsewhere. + */ + public void register(OnUxRestrictionsChangedListener listener) { + + ThreadUtils.runOnMain( + () -> { + mObservers.add(listener); + listener.onRestrictionsChanged(mCarUxRestrictions); + }); + } + + /** Unregisters a registered listener */ + public void unregister(OnUxRestrictionsChangedListener listener) { + ThreadUtils.runOnMain(() -> mObservers.remove(listener)); + } + + @NonNull + public CarUxRestrictions getCurrentRestrictions() { + return mCarUxRestrictions; + } + + /** + * Returns whether any of the given flags are blocked by the specified restrictions. If null is + * given, the method returns true for safety. + */ + public static boolean isRestricted( + @CarUxRestrictionsInfo int restrictionFlags, @Nullable CarUxRestrictions uxr) { + return (uxr == null) || ((uxr.getActiveRestrictions() & restrictionFlags) != 0); + } + + /** Sets car UX restrictions. Only used for testing. */ + @VisibleForTesting + public void setUxRestrictions(CarUxRestrictions carUxRestrictions) { + mCarUxRestrictions = carUxRestrictions; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ClusterIconContentProvider.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ClusterIconContentProvider.kt new file mode 100644 index 0000000..8f757a1 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ClusterIconContentProvider.kt @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import android.content.ContentProvider +import android.content.ContentValues +import android.content.Context +import android.content.UriMatcher +import android.database.Cursor +import android.database.MatrixCursor +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.os.ParcelFileDescriptor +import androidx.core.content.contentValuesOf +import androidx.core.graphics.scale +import androidx.core.net.toUri +import com.android.car.libraries.apphost.logging.L +import com.android.car.libraries.apphost.logging.LogTags +import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction.HOST_FAILURE_CLUSTER_ICON +import com.android.car.libraries.templates.host.internal.ClusterIconContentProvider.Companion.addToCache +import com.android.car.libraries.templates.host.internal.ClusterIconContentProvider.Companion.queryIconData +import com.google.common.cache.Cache +import com.google.common.cache.CacheBuilder +import java.util.concurrent.TimeUnit +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import com.android.car.libraries.templates.host.R + +/** + * A [ContentProvider] for providing navigation state icons to the Cluster. + * + * It uses an in-memory cache of [Bitmap]s that can be retrieved with a [Uri]. Use [addToCache] to + * add a bitmap to the cache. Use [queryIconData] to check for the existence of a bitmap in cache + * (note that this will refresh the validity of the entry). + */ +class ClusterIconContentProvider : ContentProvider() { + private val scope = CoroutineScope(Dispatchers.IO) + private lateinit var iconProviderDelegate: IconProviderDelegate + + override fun onCreate(): Boolean { + val context = checkNotNull(context) + + val timeoutMillis = + context.resources.getInteger(R.integer.cluster_icon_cache_duration_millis).toLong() + + val authority = authority(context) + iconProviderDelegate = IconProviderDelegate(authority, timeoutMillis, scope) + + return true + } + + override fun shutdown() { + scope.cancel("ContentProvider shutting down") + iconProviderDelegate.shutdown() + super.shutdown() + } + + override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor { + return iconProviderDelegate.openFile(uri) + } + + /** + * Get the uri and aspect ratio of an icon if it exists in cache. Prefer to use the convenience + * method [queryIconData], instead of calling this directly. + * + * @param selection the iconId to look up + * @return a [Cursor] with or 1 row if a match was found, or 0 rows otherwise + */ + override fun query( + uri: Uri, + projection: Array<String>?, + selection: String?, + selectionArgs: Array<String>?, + sortOrder: String? + ): Cursor { + val cursor = + MatrixCursor( + arrayOf(QUERY_RESULT_CONTENT_URI, QUERY_RESULT_ASPECT_RATIO), + /* initialCapacity */ 1 + ) + iconProviderDelegate.query(selection)?.let { (contentUri, aspectRatio) -> + cursor.addRow(arrayOf(contentUri, aspectRatio)) + } + + return cursor + } + + /** + * Converts the provided [ByteArray] to a Bitmap and caches it, returning the URI path for this + * icon. There are no stability guarantees for the keys / expected values, so prefer to use the + * convenience method [addToCache], instead of calling this directly. + */ + override fun insert(uri: Uri, values: ContentValues?): Uri? { + if (values == null) return null + val iconId = values.getAsString(INSERT_PARAM_ICON_ID) ?: return null + val bytes = values.getAsByteArray(INSERT_PARAM_BITMAP_BYTES) ?: return null + return iconProviderDelegate.cacheIcon(bytes, iconId) + } + + override fun getType(uri: Uri): String? = null + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = 0 + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array<String>? + ): Int = 0 + + companion object { + + private const val INSERT_PARAM_ICON_ID = "iconId" + private const val INSERT_PARAM_BITMAP_BYTES = "data" + + private const val QUERY_RESULT_CONTENT_URI = "contentUri" + private const val QUERY_RESULT_ASPECT_RATIO = "aspectRatio" + + /** + * @return a uri and aspect ratio for the icon if it already exists in cache. [null] otherwise + */ + fun queryIconData(iconId: String, context: Context): Pair<String, Double>? { + context.contentResolver.query(contentUri(context), null, iconId, null, null)?.use { cursor -> + if (cursor.moveToFirst()) { + val contentUriIndex = cursor.getColumnIndex(QUERY_RESULT_CONTENT_URI) + val aspectRatioIndex = cursor.getColumnIndex(QUERY_RESULT_ASPECT_RATIO) + if (contentUriIndex >= 0 && aspectRatioIndex >= 0) { + val contentUri = cursor.getString(contentUriIndex) + val aspectRatio = cursor.getDouble(aspectRatioIndex) + return contentUri to aspectRatio + } else { + L.w(LogTags.CLUSTER) { + "Icon for id $iconId exists, but failed to extract URI/aspectRatio" + } + } + } + } + return null + } + + /** Saves the bitmap to an in-memory cache, and returns a Uri that can be used to access it. */ + fun addToCache(iconId: String, bitmapBytes: ByteArray, context: Context): Uri? { + return context.contentResolver.insert( + contentUri(context), + contentValuesOf(INSERT_PARAM_ICON_ID to iconId, INSERT_PARAM_BITMAP_BYTES to bitmapBytes) + ) + } + + private fun contentUri(context: Context) = "content://${authority(context)}".toUri() + + /** Returns the provider's authority, as defined in the Manifest. */ + private fun authority(context: Context) = "${context.packageName}.ClusterIconContentProvider" + } +} + +/** + * This class extracts most of the logic out of [ClusterIconContentProvider] so it can be more + * easily tested. + */ +class IconProviderDelegate( + private val authority: String, + cacheTimeoutMillis: Long, + private val coroutineScope: CoroutineScope +) { + private var cache: Cache<String, Bitmap> = + CacheBuilder.newBuilder().expireAfterAccess(cacheTimeoutMillis, TimeUnit.MILLISECONDS).build() + + private val uriMatcher = + UriMatcher(UriMatcher.NO_MATCH).apply { addURI(authority, "img/*", URI_IMAGE_CODE) } + + fun cacheIcon(bytes: ByteArray, iconId: String): Uri { + val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + val key = keyForIconId(iconId) + + cache.put(key, bitmap) + + return uriForKey(key) + } + + /** @return a [Pair] with ContentUri and AspectRatio if a match was found, [null] otherwise */ + fun query(iconId: String?): Pair<Uri, Double>? { + if (iconId == null) return null + val key = keyForIconId(iconId) + val bitmap = cache.getIfPresent(key) ?: return null + + val contentUri = uriForKey(key) + val aspectRatio = bitmap.width.toDouble() / bitmap.height.toDouble() + return contentUri to aspectRatio + } + + /** Returns a [ParcelFileDescriptor] that will be written to asynchronously */ + fun openFile(uri: Uri): ParcelFileDescriptor { + return when (uriMatcher.match(uri)) { + URI_IMAGE_CODE -> { + val key = + requireNotNull(uri.lastPathSegment) { + "Cluster icon requested but no key provided. URI=$uri" + } + + val bitmap = + cache.getIfPresent(key) + ?: run { + LogUtil.log(HOST_FAILURE_CLUSTER_ICON) + throw IllegalStateException("Requested cluster icon that's not in cache. (key=$key)") + } + val width = uri.getQueryParameter("w")?.toIntOrNull() ?: bitmap.width + val height = uri.getQueryParameter("h")?.toIntOrNull() ?: bitmap.height + // TODO(b/197754774): Cache scaled bitmaps + val scaledBitmap = + if (width != bitmap.width || height != bitmap.height) { + bitmap.scale(width, height) + } else { + bitmap + } + + // Use a pipe to avoid eagerly saving bitmaps to disk (or at all) + val (readPipe, writePipe) = ParcelFileDescriptor.createReliablePipe() + + // asynchronously write bitmap to output stream + writeToPipeAsync(writePipe, scaledBitmap) + + // Give the readPipe for cluster to consume the bitmap + readPipe + } + else -> + throw IllegalArgumentException("Requested a path that doesn't correspond to an icon: $uri") + } + } + + private fun writeToPipeAsync(writePipe: ParcelFileDescriptor, bitmap: Bitmap) = + coroutineScope.launch { + runCatching { + L.d(LogTags.CLUSTER) { "Writing bitmap to pipe" } + ParcelFileDescriptor.AutoCloseOutputStream(writePipe).use { outputStream -> + bitmap.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, outputStream) + } + } + .onFailure { + L.e(LogTags.CLUSTER, it) { "IOException writing cluster icon to pipe" } + writePipe.closeWithError("IOException writing to pipe") + } + } + + fun shutdown() { + cache.invalidateAll() + } + + private fun keyForIconId(iconId: String) = "cluster_icon_$iconId" + + private fun uriForKey(key: String) = "content://${authority}/img/$key".toUri() + + companion object { + private const val URI_IMAGE_CODE = 1 + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ColorContrastCheckStateImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ColorContrastCheckStateImpl.kt new file mode 100644 index 0000000..21e694e --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ColorContrastCheckStateImpl.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import com.android.car.libraries.apphost.common.ColorContrastCheckState + +/** Manages the state of color contrast checks in template apps. */ +class ColorContrastCheckStateImpl : ColorContrastCheckState { + private var checkPassed = true + override fun setCheckPassed(passed: Boolean) { + checkPassed = passed + } + + override fun getCheckPassed(): Boolean { + return checkPassed + } + + override fun checksContrast(): Boolean { + return true + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CommonUtils.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CommonUtils.kt new file mode 100644 index 0000000..21b74cc --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CommonUtils.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.os.Build + +/** Holds static util methods for common usage in the host. */ +object CommonUtils { + /** + * Key for the extra that we insert into an Intent to mark it as coming from a notification + * action. + */ + const val EXTRA_NOTIFICATION_INTENT = "CAR_APP_NOTIFICATION_INTENT" + + /** Checks whether the templates host is currently running on an emulator. */ + private fun isConnectedToEmulator(): Boolean { + return Build.PRODUCT.contains("gcar") || + Build.FINGERPRINT.contains("unknown") || + Build.FINGERPRINT.contains("emu") || + Build.DEVICE.contains("generic") || + Build.DEVICE.contains("emu") + } + + /** Checks whether the templates host has debug mode enabled */ + fun isDebugEnabled(context: Context): Boolean { + return isConnectedToEmulator() || + (0 != context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ConstraintsProviderImpl.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ConstraintsProviderImpl.java new file mode 100644 index 0000000..d081426 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ConstraintsProviderImpl.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal; + +import android.car.drivingstate.CarUxRestrictions; +import android.content.Context; +import android.content.res.TypedArray; +import androidx.annotation.NonNull; +import androidx.annotation.StyleableRes; +import androidx.car.app.constraints.ConstraintManager; +import com.android.car.libraries.apphost.common.EventManager; +import com.android.car.libraries.apphost.distraction.constraints.ConstraintsProvider; +import com.android.car.libraries.templates.host.di.UxreConfig; +import com.android.car.libraries.templates.host.R; + +/** Provides different limit values for the car app. */ +public final class ConstraintsProviderImpl implements ConstraintsProvider { + private final Context mContext; + private final EventManager mEventManager; + private final UxreConfig mUxreConfig; + private final int mListMaxLength; + private final int mGridMaxLength; + + private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener mListener = + new UxRestrictionChangedListener(); + + private CarUxRestrictions mCurrentRestrictions; + + @SuppressWarnings({"ResourceType"}) + public ConstraintsProviderImpl( + Context context, EventManager eventManager, UxreConfig uxreConfig) { + mContext = context; + mEventManager = eventManager; + mUxreConfig = uxreConfig; + + CarUxRestrictionsUtil carUxRestrictionsUtil = CarUxRestrictionsUtil.getInstance(mContext); + carUxRestrictionsUtil.register(mListener); + mCurrentRestrictions = carUxRestrictionsUtil.getCurrentRestrictions(); + + @StyleableRes + final int[] themeAttrs = { + R.attr.templateListMaxLength, R.attr.templateGridMaxLength, + }; + TypedArray ta = context.obtainStyledAttributes(themeAttrs); + mListMaxLength = ta.getInt(0, 6); + mGridMaxLength = ta.getInt(1, 6); + ta.recycle(); + } + + @Override + public int getContentLimit(int contentType) { + + switch (contentType) { + case ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST: + case ConstraintManager.CONTENT_LIMIT_TYPE_LIST: + // TODO(b/186693941): For Android T and above, introduce an accurate UXRE API + // representing single-page limit so that we don't have to rely on the + // the getMaxCumulativeContentItems() API. + return mUxreConfig.getListMaxLength(mListMaxLength); + case ConstraintManager.CONTENT_LIMIT_TYPE_GRID: + // TODO(b/186693941): For Android T and above, introduce an accurate UXRE API + // representing single-page limit so that we don't have to rely on the + // the getMaxCumulativeContentItems() API. + return mUxreConfig.getGridMaxLength(mGridMaxLength); + case ConstraintManager.CONTENT_LIMIT_TYPE_PANE: + return mUxreConfig.getPaneMaxLength( + mContext.getResources().getInteger(R.integer.pane_max_length)); + case ConstraintManager.CONTENT_LIMIT_TYPE_ROUTE_LIST: + return mUxreConfig.getRouteListMaxLength( + mContext.getResources().getInteger(R.integer.route_list_max_length)); + default: + throw new IllegalArgumentException("Unknown content type: " + contentType); + } + } + + @Override + public int getTemplateStackMaxSize() { + return mUxreConfig.getTemplateStackMaxSize( + mContext.getResources().getInteger(R.integer.template_stack_max_size)); + } + + @Override + public boolean isKeyboardRestricted() { + return CarUxRestrictionsUtil.isRestricted( + CarUxRestrictions.UX_RESTRICTIONS_NO_KEYBOARD, mCurrentRestrictions); + } + + @Override + public boolean isConfigRestricted() { + return CarUxRestrictionsUtil.isRestricted( + CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP, mCurrentRestrictions); + } + + @Override + public boolean isFilteringRestricted() { + return CarUxRestrictionsUtil.isRestricted( + CarUxRestrictions.UX_RESTRICTIONS_NO_FILTERING, mCurrentRestrictions); + } + + @Override + public int getStringCharacterLimit() { + return mCurrentRestrictions.getMaxRestrictedStringLength(); + } + + void update(CarUxRestrictions restrictions) { + mCurrentRestrictions = restrictions; + mEventManager.dispatchEvent(EventManager.EventType.CONSTRAINTS); + } + + private class UxRestrictionChangedListener + implements CarUxRestrictionsUtil.OnUxRestrictionsChangedListener { + + @Override + public void onRestrictionsChanged(@NonNull CarUxRestrictions carUxRestrictions) { + update(carUxRestrictions); + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/DebugOverlayHandlerImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/DebugOverlayHandlerImpl.kt new file mode 100644 index 0000000..39018d6 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/DebugOverlayHandlerImpl.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * 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.libraries.templates.host.internal + +import androidx.car.app.model.TemplateWrapper +import com.android.car.libraries.apphost.common.DebugOverlayHandler +import java.util.LinkedHashMap + +/** The handler for the template-specific debug overlay. */ +class DebugOverlayHandlerImpl(private var isDebugOverlayActive: Boolean) : DebugOverlayHandler { + /** Using a linked hashmap here to keep track of debug entries' orders */ + private val debugTextMap: HashMap<String, String> = LinkedHashMap() + + private val builder = StringBuilder() + + private var observer: DebugOverlayHandler.Observer? = null + + override fun setActive(active: Boolean) { + isDebugOverlayActive = active + observer?.entriesUpdated() + } + + override fun isActive(): Boolean { + return isDebugOverlayActive + } + + override fun clearAllEntries() { + debugTextMap.clear() + } + + override fun removeDebugOverlayEntry(debugKey: String) { + debugTextMap.remove(debugKey) + } + + override fun updateDebugOverlayEntry(debugKey: String, debugOverlayText: String) { + debugTextMap[debugKey] = debugOverlayText + } + + override fun getDebugOverlayText(): CharSequence { + builder.setLength(0) + var needsNewLineBefore = false + for (key in debugTextMap.keys) { + if (needsNewLineBefore) { + builder.append("\n") + } + builder.append(key).append(": ").append(debugTextMap[key]) + needsNewLineBefore = true + } + return builder.toString() + } + + override fun setObserver(observer: DebugOverlayHandler.Observer?) { + this.observer = observer + observer?.entriesUpdated() + } + + override fun resetTemplateDebugOverlay(templateWrapper: TemplateWrapper) { + clearAllEntries() + updateDebugOverlayEntry( + /* debugKey= */ "Step", + Integer.toString(templateWrapper.currentTaskStep) + ) + updateDebugOverlayEntry( + /* debugKey= */ "Template", + templateWrapper.template.javaClass.simpleName + ) + observer?.entriesUpdated() + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ErrorHandlerImpl.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ErrorHandlerImpl.java new file mode 100644 index 0000000..40e8681 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ErrorHandlerImpl.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal; + +import android.content.ComponentName; +import android.content.Context; +import androidx.car.app.CarContext; +import androidx.car.app.model.MessageTemplate; +import androidx.car.app.model.TemplateWrapper; +import com.android.car.libraries.apphost.CarHost; +import com.android.car.libraries.apphost.common.CarAppError; +import com.android.car.libraries.apphost.common.CarAppManager; +import com.android.car.libraries.apphost.common.ErrorHandler; +import com.android.car.libraries.apphost.common.ErrorMessageTemplateBuilder; +import com.android.car.libraries.apphost.common.HostResourceIds; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.template.AppHost; + +/** + * Handles error cases, allowing classes that do not handle ui to be able to display an error screen + * to the user. + */ +public class ErrorHandlerImpl implements ErrorHandler { + private final Context mContext; + private final ComponentName mAppName; + private final CarAppManager mCarAppManager; + private final HostResourceIds mHostResourceIdsImpl; + + /** Returns a {@link ErrorHandlerImpl} to show an error screen */ + public static ErrorHandlerImpl create( + Context context, + ComponentName appName, + CarAppManager carAppManager, + HostResourceIds hostResourceIds) { + return new ErrorHandlerImpl(context, appName, carAppManager, hostResourceIds); + } + + private ErrorHandlerImpl( + Context context, + ComponentName appName, + CarAppManager carAppManager, + HostResourceIds hostResourceIds) { + mContext = context; + mAppName = appName; + mCarAppManager = carAppManager; + mHostResourceIdsImpl = hostResourceIds; + } + + @Override + public void showError(CarAppError error) { + Throwable cause = error.getCause(); + if (cause != null) { + if (error.logVerbose()) { + L.v(LogTags.TEMPLATE, cause, "Error: %s", error); + } else { + L.e(LogTags.TEMPLATE, cause, "Error: %s", error); + } + } else { + if (error.logVerbose()) { + L.v(LogTags.TEMPLATE, "Error: %s", error); + } else { + L.e(LogTags.TEMPLATE, "Error: %s", error); + } + } + + MessageTemplate errorMessageTemplate = + new ErrorMessageTemplateBuilder( + mContext, + error, + mHostResourceIdsImpl, + // TODO(b/183145188): finish car app should not kill the host, just + // the activity + mCarAppManager::finishCarApp) + .build(); + + CarHost carHost = CarHostRepository.INSTANCE.get(mAppName); + AppHost apphost = (AppHost) carHost.getHostOrThrow(CarContext.APP_SERVICE); + apphost.getUIController().setTemplate(mAppName, TemplateWrapper.wrap(errorMessageTemplate)); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/InputConfigImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/InputConfigImpl.kt new file mode 100644 index 0000000..f49181f --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/InputConfigImpl.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import com.android.car.libraries.apphost.input.InputConfig + +/** Manages the state of routing information in template apps. */ +class InputConfigImpl : InputConfig { + override fun hasTouchpadForUiNavigation(): Boolean { + // TODO(b/188454942): Retrieve the input configuration from AAOS system + return false + } + + override fun hasTouch(): Boolean { + // TODO(b/188454942): Retrieve the input configuration from AAOS system + return true + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/InputManagerImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/InputManagerImpl.kt new file mode 100644 index 0000000..922cbc7 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/InputManagerImpl.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.inputmethod.EditorInfo +import androidx.car.app.activity.renderer.IProxyInputConnection +import com.android.car.libraries.apphost.input.CarEditable +import com.android.car.libraries.apphost.input.CarEditableListener +import com.android.car.libraries.apphost.input.InputManager +import com.android.car.libraries.apphost.logging.LogTags + +/** The app specific implementation of [InputManager]. */ +class InputManagerImpl(private val listener: InputManagerListener) : InputManager { + + /** A listener to be notified for input related events. */ + interface InputManagerListener { + /* Should start the input, i.e. show soft keyboard */ + fun onStartInput() + + /* Should stop the input, i.e. hide soft keyboard */ + fun onStopInput() + + /* + * Update the text selection. Gets called whenever text selection changes on the + * [currentEditable]. + */ + fun onUpdateSelection(oldSelStart: Int, oldSelEnd: Int, newSelStart: Int, newSelEnd: Int) + } + + private var currentEditable: CarEditable? = null + private val handler = Handler(Looper.getMainLooper()) + + private var stopInputRunnable = Runnable { + if (isInputActive) { + currentEditable = null + listener.onStopInput() + } + } + + private val carEditableListener = + CarEditableListener { oldSelStart, oldSelEnd, newSelStart, newSelEnd -> + listener.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd) + } + + override fun startInput(view: CarEditable) { + currentEditable?.setCarEditableListener(null) + currentEditable = view + currentEditable?.setCarEditableListener(carEditableListener) + + // Cancel any ongoing stop input to avoid jarring keyboard animations. + handler.removeCallbacks(stopInputRunnable) + + listener.onStartInput() + } + + override fun stopInput() { + currentEditable?.setCarEditableListener(null) + + // Perform stop input with a delay to avoid jarring keyboard disappear+reappear animation + // when switching form one focusable to another. + handler.removeCallbacks(stopInputRunnable) + handler.postDelayed(stopInputRunnable, STOP_INPUT_DELAY_MILLIS) + } + + override fun isValid() = true + override fun isInputActive() = currentEditable != null + + fun onCreateInputConnection(editorInfo: EditorInfo): IProxyInputConnection? { + val currentEditable = + currentEditable + ?: run { + Log.d(LogTags.APP_HOST, "There is no focusable target selected.") + return null + } + val inputConnection = + currentEditable.onCreateInputConnection(editorInfo) + ?: run { + Log.d(LogTags.APP_HOST, "Failed to create input connection for editorInfo $editorInfo") + return null + } + return ProxyInputConnection(inputConnection, editorInfo) + } + + companion object { + const val STOP_INPUT_DELAY_MILLIS = 100L + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/InsetsListener.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/InsetsListener.kt new file mode 100644 index 0000000..edd4d00 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/InsetsListener.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import android.graphics.Insets +import android.os.Build +import android.view.WindowInsets +import androidx.car.app.activity.renderer.IInsetsListener +import androidx.car.app.utils.ThreadUtils +import com.android.car.libraries.apphost.logging.L +import com.android.car.libraries.apphost.logging.LogTags +import com.android.car.libraries.apphost.view.AbstractTemplateView + +/** Handles window insets from the car app. */ +class InsetsListener(private val templateView: AbstractTemplateView) : IInsetsListener.Stub() { + override fun onInsetsChanged(insets: Insets) { + ThreadUtils.runOnMain { + L.i(LogTags.APP_HOST) { "Received insets: $insets" } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + templateView.windowInsets = + WindowInsets.Builder() + .setInsets(WindowInsets.Type.systemBars() or WindowInsets.Type.ime(), insets) + .build() + } else { + templateView.windowInsets = WindowInsets.Builder().setSystemWindowInsets(insets).build() + } + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/LogUtil.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/LogUtil.kt new file mode 100644 index 0000000..c6b2272 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/LogUtil.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import android.content.ComponentName +import android.content.Context +import com.android.car.libraries.apphost.logging.L +import com.android.car.libraries.apphost.logging.LogTags +import com.android.car.libraries.apphost.logging.TelemetryEvent +import com.android.car.libraries.apphost.logging.TelemetryHandler +import com.android.car.libraries.templates.host.di.TelemetryHandlerFactory + +/** + * Holds static telemetry logging methods for common usage in the host. These methods should only be + * used by service level component, e.g. [ClusterIconContentProvider]. Car app level logging should + * use [TelemetryHandler] from TemplateContext + */ +class LogUtil(private val telemetryHandler: TelemetryHandler) { + + companion object { + private lateinit var instance: LogUtil + + fun init(telemetryHandlerFactory: TelemetryHandlerFactory, applicationContext: Context?) { + checkNotNull(applicationContext) + instance = + LogUtil( + telemetryHandlerFactory.create( + applicationContext, + ComponentName(applicationContext, LogUtil::class.java) + ) + ) + } + + fun log(uiAction: TelemetryEvent.UiAction) { + log(TelemetryEvent.newBuilder(uiAction)) + } + + private fun log(builder: TelemetryEvent.Builder) { + if (!this::instance.isInitialized) { + L.d(LogTags.APP_HOST) { "CommonLogger is not initialized" } + return + } + instance.telemetryHandler.logCarAppTelemetry(builder) + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationCoordinator.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationCoordinator.kt new file mode 100644 index 0000000..443559b --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationCoordinator.kt @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import android.car.Car +import android.car.CarAppFocusManager +import android.car.CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION as APP_TYPE_NAVIGATION +import android.car.CarAppFocusManager.OnAppFocusOwnershipCallback as FocusCallback +import android.car.cluster.navigation.NavigationState +import android.car.navigation.CarNavigationStatusManager +import android.content.Context +import androidx.annotation.VisibleForTesting +import androidx.car.app.navigation.model.Trip +import androidx.core.os.bundleOf +import com.android.car.libraries.apphost.common.CarAppPackageInfo +import com.android.car.libraries.apphost.common.TemplateContext +import com.android.car.libraries.apphost.logging.L +import com.android.car.libraries.apphost.logging.LogTags +import java.util.concurrent.atomic.AtomicBoolean +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import com.android.car.libraries.templates.host.R + + +/** + * Coordinate navigation and focus control between all Template Apps. Apps can start sending + * navigation events after requesting focus, but it's possible some events will be dropped while + * focus is being obtained. + */ +class NavigationCoordinator +private constructor( + carAppFocusManagerProvider: () -> CarAppFocusManager?, + carNavStatusManagerProvider: () -> CarNavigationStatusManager?, + private val shouldShareNavState: Boolean +) : FocusCallback { + /** + * Instances of this interface will be compared against each other for equality. Either make sure + * you are sending the same instance per app, or implement [equals] to account for this. + */ + interface NavAppFocusOwner { + val packageInfo: CarAppPackageInfo + fun onFocusLost() + } + + private val focusManager: CarAppFocusManager? by lazy(carAppFocusManagerProvider) + private val navigationManager: CarNavigationStatusManager? by lazy(carNavStatusManagerProvider) + + private val _navigationState = MutableStateFlow<HostNavState>(HostNavState.NotNavigating) + val navigationState = _navigationState.asStateFlow() + + /** Whether or not the Host has navigation focus currently */ + private val isOwningFocus = AtomicBoolean(false) + private var currentNavApp: NavAppFocusOwner? = null + + /** + * Apps must request Focus (see [requestAppFocus]) before sending navigation events. Also, there's + * a chance that some events will be dropped on the floor while focus is being obtained. + */ + fun sendNavigationStateChange( + navApp: NavAppFocusOwner, + // TODO(b/206694446): Only accept Trip and do the Proto conversion here. + navigationState: NavigationState.NavigationStateProto, + templateContext: TemplateContext, + trip: Trip? = null + ) = + synchronized(this) { + if (isFocused(navApp)) { + _navigationState.value = + if (trip != null) HostNavState.Navigating(trip, templateContext, navApp.packageInfo) + else HostNavState.NotNavigating + if (shouldShareNavState) { + navigationManager?.sendNavigationStateChange(navigationState.asBundle()) + } + } else { + L.w(LogTags.NAVIGATION) { + val packageName = navApp.packageInfo.componentName.packageName + "Package $packageName is trying to send NavigationState updates without owning focus" + } + } + } + + /** + * Note that a result of [CarAppFocusManager.APP_FOCUS_REQUEST_SUCCEEDED] does not mean you have + * focus yet. This call is asynchronous and + * [CarAppFocusManager.OnAppFocusOwnershipCallback.onAppFocusOwnershipGranted] will be called when + * focus is granted for the app. + */ + fun requestAppFocus(navApp: NavAppFocusOwner) = + synchronized(this) { + val focusManager = + focusManager + ?: run { + L.w(LogTags.NAVIGATION) { + "Couldn't obtain focusManager. Are you missing a permission?" + } + navApp.onFocusLost() + return + } + + if (navApp != currentNavApp) { + currentNavApp?.onFocusLost() + currentNavApp = navApp + } + + if (!isOwningFocus.get()) { + // request focus from system + val result = focusManager.requestAppFocus(APP_TYPE_NAVIGATION, this) + if (result == CarAppFocusManager.APP_FOCUS_REQUEST_FAILED) { + onAppFocusOwnershipLost(APP_TYPE_NAVIGATION) + } + } else { + clearNavState() + } + } + + /** Notify that [navApp] is done navigation and no longer requires focus. */ + fun abandonAppFocus(navApp: NavAppFocusOwner) = + synchronized(this) { + if (isFocused(navApp)) { + onAppFocusOwnershipLost(APP_TYPE_NAVIGATION) + focusManager?.abandonAppFocus(this, APP_TYPE_NAVIGATION) + _navigationState.value = HostNavState.NotNavigating + } + } + + override fun onAppFocusOwnershipLost(appType: Int) = + synchronized(this) { + L.d(LogTags.NAVIGATION) { "Host focus Lost" } + isOwningFocus.set(false) + currentNavApp?.onFocusLost() + currentNavApp = null + } + + override fun onAppFocusOwnershipGranted(appType: Int) = + synchronized(this) { + L.d(LogTags.NAVIGATION) { "Host focus granted" } + isOwningFocus.set(true) + if (!shouldShareNavState) { + L.d(LogTags.NAVIGATION, "NavState data will not sent to system.") + clearNavState() + } + } + + private fun clearNavState() { + val emptyNavState = NavigationState.NavigationStateProto.getDefaultInstance() + navigationManager?.sendNavigationStateChange(emptyNavState.asBundle()) + } + + /** returns whether or not [navApp] has focus currently */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun isFocused(navApp: NavAppFocusOwner): Boolean = + synchronized(this) { isOwningFocus.get() && this.currentNavApp == navApp } + + companion object { + private lateinit var instance: NavigationCoordinator + + fun getInstance(context: Context): NavigationCoordinator { + if (!this::instance.isInitialized) { + val themeAttrs = intArrayOf(R.attr.templateSendNavStateToSystem) + val ta = context.obtainStyledAttributes(themeAttrs) + val shouldShareNavState = + ta.getBoolean(0, context.resources.getBoolean(R.bool.send_navstates_to_system)) + ta.recycle() + instance = + NavigationCoordinator( + carAppFocusManagerProvider = { context.getCarService(Car.APP_FOCUS_SERVICE) }, + carNavStatusManagerProvider = { context.getCarService(Car.CAR_NAVIGATION_SERVICE) }, + shouldShareNavState + ) + } + return instance + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + fun testInstance( + carAppFocusManager: CarAppFocusManager, + carNavStatusManager: CarNavigationStatusManager + ) = NavigationCoordinator({ carAppFocusManager }, { carNavStatusManager }, true) + + private inline fun <reified T> Context.getCarService(serviceName: String): T? { + val car: Car? = Car.createCar(this) + if (car == null) { + L.e(LogTags.NAVIGATION) { "Nav state disabled: Unable to connect to CarService" } + return null + } + return runCatching { car.getCarManager(serviceName) as T? } + .onSuccess { L.d(LogTags.NAVIGATION) { "Obtained service: $serviceName" } } + .onFailure { + L.e(LogTags.NAVIGATION, it) { + "Nav state disabled: Unable to obtain access to $serviceName." + } + } + .getOrNull() + } + } +} + +private const val NAVIGATION_STATE_PROTO_BUNDLE_KEY = "navstate2" + +private fun NavigationState.NavigationStateProto.asBundle() = + bundleOf(NAVIGATION_STATE_PROTO_BUNDLE_KEY to this.toByteArray()) + +sealed class HostNavState { + object NotNavigating : HostNavState() + class Navigating( + val trip: Trip, + val templateContext: TemplateContext, + val packageInfo: CarAppPackageInfo + ) : HostNavState() +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationStateCallbackImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationStateCallbackImpl.kt new file mode 100644 index 0000000..80d68fc --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationStateCallbackImpl.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import android.car.cluster.navigation.NavigationState +import androidx.car.app.navigation.model.Trip +import com.android.car.libraries.apphost.common.CarAppPackageInfo +import com.android.car.libraries.apphost.common.TemplateContext +import com.android.car.libraries.apphost.logging.L +import com.android.car.libraries.apphost.logging.LogTags +import com.android.car.libraries.apphost.nav.NavigationHost +import com.android.car.libraries.apphost.nav.NavigationStateCallback +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import com.android.car.libraries.templates.host.R + +/** Handles navigation state change events from [NavigationHost]. */ +class NavigationStateCallbackImpl +private constructor( + private val templateContext: TemplateContext, + private val navigationStateConverter: NavigationStateConverter +) : NavigationStateCallback { + private var onNavigationStopRunnable: Runnable? = null + + private val packageInfo = templateContext.carAppPackageInfo + private val navigationCoordinator by lazy { NavigationCoordinator.getInstance(templateContext) } + + private val navApp = + object : NavigationCoordinator.NavAppFocusOwner { + override val packageInfo: CarAppPackageInfo + get() = templateContext.carAppPackageInfo + + override fun onFocusLost() { + onNavigationStopRunnable?.run() + } + } + + override fun onUpdateTrip(trip: Trip): Boolean { + L.v(LogTags.NAVIGATION) { "onUpdateTrip $packageInfo" } + CoroutineScope(Dispatchers.Default).launch { + // Conversion shouldn't take a long time. If it hangs for too long, just kill it. + val timeMillis = + templateContext + .resources + .getInteger(R.integer.cluster_trip_to_navstate_conversion_timeout_millis) + .toLong() + val navigationState = + withTimeout(timeMillis) { navigationStateConverter.tripToNavigationState(trip) } + navigationCoordinator.sendNavigationStateChange( + navApp, + navigationState, + templateContext, + trip + ) + } + return true + } + + override fun onNavigationStarted(onNavigationStopRunnable: Runnable) { + L.v(LogTags.NAVIGATION) { "onNavigationStarted ${templateContext.carAppPackageInfo}" } + + this.onNavigationStopRunnable = onNavigationStopRunnable + if (templateContext.carHostConfig.isClusterEnabled) { + navigationCoordinator.requestAppFocus(navApp) + } + } + + override fun onNavigationEnded() { + L.v(LogTags.NAVIGATION) { "onNavigationEnded ${templateContext.carAppPackageInfo}" } + + // Remove directions from cluster + navigationCoordinator.sendNavigationStateChange( + navApp, + NavigationState.NavigationStateProto.getDefaultInstance(), + templateContext + ) + + navigationCoordinator.abandonAppFocus(navApp) + + onNavigationStopRunnable = null + } + + companion object { + fun create(templateContext: TemplateContext): NavigationStateCallback { + return NavigationStateCallbackImpl( + templateContext, + NavigationStateConverterImpl(templateContext) + ) + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationStateConverter.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationStateConverter.kt new file mode 100644 index 0000000..2510f4d --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationStateConverter.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import android.car.cluster.navigation.NavigationState +import android.car.navigation.CarNavigationStatusManager +import androidx.car.app.navigation.NavigationManager +import androidx.car.app.navigation.model.Trip + +/** + * Convert a [Trip] (from [NavigationManager]) to a [NavigationState.NavigationStateProto] that the + * Cluster can parse and display (via [CarNavigationStatusManager.sendNavigationStateChange]). + */ +interface NavigationStateConverter { + suspend fun tripToNavigationState(trip: Trip): NavigationState.NavigationStateProto +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationStateConverterImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationStateConverterImpl.kt new file mode 100644 index 0000000..2ff9d6a --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationStateConverterImpl.kt @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import android.car.cluster.navigation.CueKt.cueElement +import android.car.cluster.navigation.LaneKt.laneDirection +import android.car.cluster.navigation.NavigationState +import android.car.cluster.navigation.NavigationState.Lane.LaneDirection.Shape +import android.car.cluster.navigation.NavigationState.Maneuver.Type as ManeuverType +import android.car.cluster.navigation.NavigationState.NavigationStateProto.ServiceStatus.NORMAL +import android.car.cluster.navigation.NavigationState.NavigationStateProto.ServiceStatus.REROUTING +import android.car.cluster.navigation.cue +import android.car.cluster.navigation.destination +import android.car.cluster.navigation.distance +import android.car.cluster.navigation.lane +import android.car.cluster.navigation.maneuver +import android.car.cluster.navigation.navigationStateProto +import android.car.cluster.navigation.road +import android.car.cluster.navigation.step +import android.car.cluster.navigation.timestamp +import android.car.navigation.CarNavigationStatusManager +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import androidx.car.app.model.CarIcon +import androidx.car.app.model.CarText +import androidx.car.app.model.Distance +import androidx.car.app.navigation.NavigationManager +import androidx.car.app.navigation.model.Lane +import androidx.car.app.navigation.model.LaneDirection +import androidx.car.app.navigation.model.Maneuver +import androidx.car.app.navigation.model.TravelEstimate +import androidx.car.app.navigation.model.Trip +import androidx.core.graphics.drawable.IconCompat +import androidx.core.graphics.drawable.toBitmap +import com.android.car.libraries.apphost.common.TemplateContext +import com.android.car.libraries.apphost.logging.L +import com.android.car.libraries.apphost.logging.LogTags +import com.android.car.libraries.apphost.view.common.DateTimeUtils +import com.android.car.libraries.apphost.view.common.DistanceUtils +import com.android.car.libraries.apphost.view.common.ImageUtils +import com.android.car.libraries.apphost.view.common.ImageViewParams +import java.io.ByteArrayOutputStream +import java.time.Duration +import java.util.UUID +import kotlinx.coroutines.coroutineScope + +/** + * Convert a [Trip] (from [NavigationManager]) to a [NavigationState.NavigationStateProto] that the + * Cluster can parse and display (via [CarNavigationStatusManager.sendNavigationStateChange]). + */ +class NavigationStateConverterImpl(private val templateContext: TemplateContext) : + NavigationStateConverter { + + /** If the Provider gave an error once, we don't want to keep hitting it */ + private var skipIcons = false + + override suspend fun tripToNavigationState(trip: Trip) = coroutineScope { + navigationStateProto { + serviceStatus = if (trip.isLoading) REROUTING else NORMAL + trip.currentRoad?.let { currentRoad = road { name = it.toString() } } + steps += trip.getNavigationStateSteps() + destinations += trip.getNavigationStateDestinations() + } + } + + private fun Trip.getNavigationStateDestinations(): List<NavigationState.Destination> = + destinations.zip(destinationTravelEstimates).map { (destination, estimate) -> + destination { + destination.name?.toString()?.let { title = it } + destination.address?.toString()?.let { address = it } + distance = estimate.toNavStateDistance() + estimate.arrivalTimeAtDestination?.timeSinceEpochMillis?.let { epochMillis -> + estimatedTimeAtArrival = timestamp { seconds = epochMillis / 1000 } + } + formattedDurationUntilArrival = estimate.getFormattedRemainingDuration(templateContext) + estimate.arrivalTimeAtDestination?.zoneShortName?.let { zoneId = it } + } + } + + private fun Trip.getNavigationStateSteps(): List<NavigationState.Step> { + return steps.zip(stepTravelEstimates).map { (step, estimate) -> + step { + step.maneuver?.let { maneuver = it.toNavStateManeuver() } + distance = estimate.toNavStateDistance() + step.cue?.let { cue = it.toNavStateCue() } + lanes += step.lanes.map { it.toNavStateLane() } + step.lanesImage?.toImageReference()?.let { lanesImage = it } + } + } + } + + private fun Lane.toNavStateLane() = lane { + laneDirections += + directions.map { laneDirection -> + laneDirection { + shape = laneDirection.shape.toNavStateShape() + isHighlighted = laneDirection.isRecommended + } + } + } + + private fun Int.toNavStateShape() = + when (this) { + LaneDirection.SHAPE_UNKNOWN -> Shape.UNKNOWN + LaneDirection.SHAPE_STRAIGHT -> Shape.STRAIGHT + LaneDirection.SHAPE_SLIGHT_LEFT -> Shape.SLIGHT_LEFT + LaneDirection.SHAPE_SLIGHT_RIGHT -> Shape.SLIGHT_RIGHT + LaneDirection.SHAPE_NORMAL_LEFT -> Shape.NORMAL_LEFT + LaneDirection.SHAPE_NORMAL_RIGHT -> Shape.NORMAL_RIGHT + LaneDirection.SHAPE_SHARP_LEFT -> Shape.SHARP_LEFT + LaneDirection.SHAPE_SHARP_RIGHT -> Shape.SHARP_RIGHT + else -> Shape.UNRECOGNIZED + } + + private fun CarText.toNavStateCue() = cue { + val cueText = this@toNavStateCue.toString() + alternateText = cueText + elements += cueElement { text = cueText } + } + + private fun TravelEstimate.toNavStateDistance() = distance { + meters = DistanceUtils.getMeters(remainingDistance) + remainingDistance?.let { + displayValue = DistanceUtils.convertDistanceToDisplayStringNoUnit(templateContext, it) + } + displayUnits = + when (remainingDistance?.displayUnit) { + Distance.UNIT_METERS -> NavigationState.Distance.Unit.METERS + Distance.UNIT_KILOMETERS, Distance.UNIT_KILOMETERS_P1 -> + NavigationState.Distance.Unit.KILOMETERS + Distance.UNIT_MILES, Distance.UNIT_MILES_P1 -> NavigationState.Distance.Unit.MILES + Distance.UNIT_FEET -> NavigationState.Distance.Unit.FEET + Distance.UNIT_YARDS -> NavigationState.Distance.Unit.YARDS + else -> NavigationState.Distance.Unit.UNKNOWN + } + } + + /** + * Only Resource/Bitmap icons are supported. Will return [null] for all other types of [CarIcon] + */ + private fun CarIcon.toImageReference(): NavigationState.ImageReference? { + if (skipIcons) return null + + val iconId = hash(this).toString() + + // Don't extract a Drawable unless needed + ClusterIconContentProvider.queryIconData(iconId, templateContext)?.let { + (contentUri, aspectRatio) -> + return NavigationState.ImageReference.newBuilder() + .setContentUri(contentUri) + .setAspectRatio(aspectRatio) + .build() + } + + // No cache for icon, get Drawable and cache it + val drawable = + ImageUtils.getIconDrawable(templateContext, this, ImageViewParams.DEFAULT) + ?: run { + L.d(LogTags.NAVIGATION) { + "Couldn't obtain Drawable from CarIcon (uri icons not supported): $this" + } + return null + } + + val aspectRatio = + if (drawable.intrinsicWidth > 0 && drawable.intrinsicHeight > 0) { + drawable.intrinsicWidth.toDouble() / drawable.intrinsicHeight.toDouble() + } else { + L.w(LogTags.NAVIGATION) { + "Drawable has no intrinsic dimensions aspect ratio. carIcon=$this" + } + return null + } + + val contentUri = + runCatching { + val bytes = drawable.toByteArray() + ClusterIconContentProvider.addToCache(iconId, bytes, templateContext) + } + .onFailure { + skipIcons = true + L.w(LogTags.NAVIGATION, it) { + "Failed to cache icon in provider." + + " Disabling cluster icons for ${templateContext.appPackageName}" + } + } + .getOrNull() + ?.toString() + ?: return null + + return NavigationState.ImageReference.newBuilder() + .setContentUri(contentUri) + .setAspectRatio(aspectRatio) + .build() + } + + private fun Drawable.toByteArray(): ByteArray { + val bitmap = this.toBitmap() + val stream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) + return stream.toByteArray() + } + + private fun hash(icon: CarIcon): Int { + return when (icon.icon?.type) { + IconCompat.TYPE_RESOURCE, IconCompat.TYPE_URI, null -> icon.hashCode() + else -> { + // For any iconCompat type that exists and isn't URI or Resource, we don't really + // know how to tell if two instances represent the same set of pixels. + // So we just consider them unique. + UUID.randomUUID().hashCode() + } + } + } + + private fun Maneuver.toNavStateManeuver() = maneuver { + val maneuver = this@toNavStateManeuver + type = maneuver.getNavStateType() + roundaboutExitNumber = maneuver.roundaboutExitNumber + maneuver.icon?.toImageReference()?.let { icon = it } + } + + private fun Maneuver.getNavStateType(): ManeuverType = + when (type) { + Maneuver.TYPE_UNKNOWN -> ManeuverType.UNKNOWN + Maneuver.TYPE_DEPART -> ManeuverType.DEPART + Maneuver.TYPE_NAME_CHANGE -> ManeuverType.NAME_CHANGE + Maneuver.TYPE_KEEP_LEFT -> ManeuverType.KEEP_LEFT + Maneuver.TYPE_KEEP_RIGHT -> ManeuverType.KEEP_RIGHT + Maneuver.TYPE_TURN_SLIGHT_LEFT -> ManeuverType.TURN_SLIGHT_LEFT + Maneuver.TYPE_TURN_SLIGHT_RIGHT -> ManeuverType.TURN_SLIGHT_RIGHT + Maneuver.TYPE_TURN_NORMAL_LEFT -> ManeuverType.TURN_NORMAL_LEFT + Maneuver.TYPE_TURN_NORMAL_RIGHT -> ManeuverType.TURN_NORMAL_RIGHT + Maneuver.TYPE_TURN_SHARP_LEFT -> ManeuverType.TURN_SHARP_LEFT + Maneuver.TYPE_TURN_SHARP_RIGHT -> ManeuverType.TURN_SHARP_RIGHT + Maneuver.TYPE_U_TURN_LEFT -> ManeuverType.U_TURN_LEFT + Maneuver.TYPE_U_TURN_RIGHT -> ManeuverType.U_TURN_RIGHT + Maneuver.TYPE_ON_RAMP_SLIGHT_LEFT -> ManeuverType.ON_RAMP_SLIGHT_LEFT + Maneuver.TYPE_ON_RAMP_SLIGHT_RIGHT -> ManeuverType.ON_RAMP_SLIGHT_RIGHT + Maneuver.TYPE_ON_RAMP_NORMAL_LEFT -> ManeuverType.ON_RAMP_NORMAL_LEFT + Maneuver.TYPE_ON_RAMP_NORMAL_RIGHT -> ManeuverType.ON_RAMP_NORMAL_RIGHT + Maneuver.TYPE_ON_RAMP_SHARP_LEFT -> ManeuverType.ON_RAMP_SHARP_LEFT + Maneuver.TYPE_ON_RAMP_SHARP_RIGHT -> ManeuverType.ON_RAMP_SHARP_RIGHT + Maneuver.TYPE_ON_RAMP_U_TURN_LEFT -> ManeuverType.ON_RAMP_U_TURN_LEFT + Maneuver.TYPE_ON_RAMP_U_TURN_RIGHT -> ManeuverType.ON_RAMP_U_TURN_RIGHT + Maneuver.TYPE_OFF_RAMP_SLIGHT_LEFT -> ManeuverType.OFF_RAMP_SLIGHT_LEFT + Maneuver.TYPE_OFF_RAMP_SLIGHT_RIGHT -> ManeuverType.OFF_RAMP_SLIGHT_RIGHT + Maneuver.TYPE_OFF_RAMP_NORMAL_LEFT -> ManeuverType.OFF_RAMP_NORMAL_LEFT + Maneuver.TYPE_OFF_RAMP_NORMAL_RIGHT -> ManeuverType.OFF_RAMP_NORMAL_RIGHT + Maneuver.TYPE_FORK_LEFT -> ManeuverType.FORK_LEFT + Maneuver.TYPE_FORK_RIGHT -> ManeuverType.FORK_RIGHT + Maneuver.TYPE_MERGE_LEFT -> ManeuverType.MERGE_LEFT + Maneuver.TYPE_MERGE_RIGHT -> ManeuverType.MERGE_RIGHT + Maneuver.TYPE_MERGE_SIDE_UNSPECIFIED -> ManeuverType.MERGE_SIDE_UNSPECIFIED + Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW + Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE -> { + when (roundaboutExitAngle) { + in 6..45 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW_SHARP_LEFT + in 46..135 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW_NORMAL_LEFT + in 136..170 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW_SLIGHT_LEFT + in 171..189 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW_STRAIGHT + in 190..224 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW_SLIGHT_RIGHT + in 225..314 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW_NORMAL_RIGHT + in 315..354 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW_SHARP_RIGHT + in 1..5, in 355..360 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW_U_TURN + else -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW + } + } + Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW + Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE -> { + when (roundaboutExitAngle) { + in 6..45 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW_SHARP_RIGHT + in 46..135 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW_NORMAL_RIGHT + in 136..170 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW_SLIGHT_RIGHT + in 171..189 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW_STRAIGHT + in 190..224 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW_SLIGHT_LEFT + in 225..314 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW_NORMAL_LEFT + in 315..354 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW_SHARP_LEFT + in 1..5, in 355..360 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW_U_TURN + else -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW + } + } + Maneuver.TYPE_STRAIGHT -> ManeuverType.STRAIGHT + Maneuver.TYPE_FERRY_BOAT -> ManeuverType.FERRY_BOAT + Maneuver.TYPE_FERRY_TRAIN -> ManeuverType.FERRY_TRAIN + Maneuver.TYPE_DESTINATION -> ManeuverType.DESTINATION + Maneuver.TYPE_DESTINATION_STRAIGHT -> ManeuverType.DESTINATION_STRAIGHT + Maneuver.TYPE_DESTINATION_LEFT -> ManeuverType.DESTINATION_LEFT + Maneuver.TYPE_DESTINATION_RIGHT -> ManeuverType.DESTINATION_RIGHT + Maneuver.TYPE_ROUNDABOUT_ENTER_CW, Maneuver.TYPE_ROUNDABOUT_EXIT_CW -> + ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW + Maneuver.TYPE_ROUNDABOUT_ENTER_CCW, Maneuver.TYPE_ROUNDABOUT_EXIT_CCW -> + ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW + Maneuver.TYPE_FERRY_BOAT_LEFT, + Maneuver.TYPE_FERRY_BOAT_RIGHT, + Maneuver.TYPE_FERRY_TRAIN_LEFT, + Maneuver.TYPE_FERRY_TRAIN_RIGHT -> ManeuverType.FERRY_TRAIN + else -> ManeuverType.UNKNOWN + } + + private fun TravelEstimate.getFormattedRemainingDuration(templateContext: TemplateContext) = + if (remainingTimeSeconds == TravelEstimate.REMAINING_TIME_UNKNOWN) "" + else + DateTimeUtils.formatDurationString(templateContext, Duration.ofSeconds(remainingTimeSeconds)) +} + +/** Just a convenience to get the Client package name */ +private val TemplateContext.appPackageName + get() = carAppPackageInfo.componentName.packageName diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ProxyInputConnection.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ProxyInputConnection.kt new file mode 100644 index 0000000..a7f0b59 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ProxyInputConnection.kt @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.KeyEvent +import android.view.inputmethod.CompletionInfo +import android.view.inputmethod.CorrectionInfo +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.ExtractedText +import android.view.inputmethod.ExtractedTextRequest +import android.view.inputmethod.InputConnection +import androidx.car.app.activity.renderer.IProxyInputConnection +import java.util.concurrent.Callable +import java.util.concurrent.ExecutionException +import java.util.concurrent.FutureTask +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +/** + * Proxies an [InputConnection] across a binder interface. All InputConnection calls are made on the + * main thread. + * + * Please note that once an InputConnection is invalid, it never becomes valid again. An invalid + * InputConnection simply ignores calls that are made to it. + * + * Some InputConnection methods simply return a boolean indicating whether the input connection is + * still valid. For these methods, we run the action and update the validity of the input connection + * asynchronously - there's no need to synchronize this so long as the action happens on the main + * thread. For all other methods where the return value matters, we block on the Binder thread until + * the value has been provided on the main thread. + */ +class ProxyInputConnection( + private val inputConnection: InputConnection, + private val editorInfo: EditorInfo +) : IProxyInputConnection.Stub() { + @Volatile private var inputConnectionValid = true + private val handler = Handler(Looper.getMainLooper()) + + override fun getTextBeforeCursor(n: Int, flags: Int): CharSequence? { + return runOnMainAndAwaitResult(null) { inputConnection.getTextBeforeCursor(n, flags) } + } + + override fun getTextAfterCursor(n: Int, flags: Int): CharSequence? { + return runOnMainAndAwaitResult(null) { inputConnection.getTextAfterCursor(n, flags) } + } + + override fun getSelectedText(flags: Int): CharSequence? { + return runOnMainAndAwaitResult(null) { inputConnection.getSelectedText(flags) } + } + + override fun getCursorCapsMode(reqModes: Int): Int { + return runOnMainAndAwaitResult(0) { inputConnection.getCursorCapsMode(reqModes) } + } + + override fun beginBatchEdit(): Boolean { + return runOnMainAndAwaitResult(false) { inputConnection.beginBatchEdit() } + } + + override fun endBatchEdit(): Boolean { + return runOnMainAndAwaitResult(false) { inputConnection.endBatchEdit() } + } + + override fun sendKeyEvent(event: KeyEvent): Boolean { + return runOnMainAndAwaitResult(false) { inputConnection.sendKeyEvent(event) } + } + + override fun commitCorrection(correctionInfo: CorrectionInfo): Boolean { + return runOnMainAndAwaitResult(false) { inputConnection.commitCorrection(correctionInfo) } + } + + override fun commitCompletion(text: CompletionInfo?): Boolean { + return runOnMainAndAwaitResult(false) { inputConnection.commitCompletion(text) } + } + + override fun getExtractedText(request: ExtractedTextRequest?, flags: Int): ExtractedText? { + return runOnMainAndAwaitResult(null) { inputConnection.getExtractedText(request, flags) } + } + + override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean { + return runOnMainAndUpdateValidity { + inputConnection.deleteSurroundingText(beforeLength, afterLength) + } + } + + override fun setComposingText(text: CharSequence, newCursorPosition: Int): Boolean { + return runOnMainAndUpdateValidity { inputConnection.setComposingText(text, newCursorPosition) } + } + + override fun setComposingRegion(start: Int, end: Int): Boolean { + return runOnMainAndUpdateValidity { inputConnection.setComposingRegion(start, end) } + } + + override fun finishComposingText(): Boolean { + return runOnMainAndUpdateValidity { inputConnection.finishComposingText() } + } + + override fun commitText(text: CharSequence, newCursorPosition: Int): Boolean { + return runOnMainAndUpdateValidity { inputConnection.commitText(text, newCursorPosition) } + } + + override fun setSelection(start: Int, end: Int): Boolean { + return runOnMainAndUpdateValidity { inputConnection.setSelection(start, end) } + } + + override fun performEditorAction(editorAction: Int): Boolean { + return runOnMainAndUpdateValidity { inputConnection.performEditorAction(editorAction) } + } + + override fun performContextMenuAction(id: Int): Boolean { + return runOnMainAndUpdateValidity { inputConnection.performContextMenuAction(id) } + } + + override fun clearMetaKeyStates(states: Int): Boolean { + return runOnMainAndUpdateValidity { inputConnection.clearMetaKeyStates(states) } + } + + override fun reportFullscreenMode(enabled: Boolean): Boolean { + return runOnMainAndUpdateValidity { inputConnection.reportFullscreenMode(enabled) } + } + + override fun performPrivateCommand(action: String, data: Bundle): Boolean { + return runOnMainAndUpdateValidity { inputConnection.performPrivateCommand(action, data) } + } + + override fun requestCursorUpdates(cursorUpdateMode: Int): Boolean { + return runOnMainAndUpdateValidity { inputConnection.requestCursorUpdates(cursorUpdateMode) } + } + + override fun closeConnection() { + runOnMainDirect { + inputConnection.closeConnection() + inputConnectionValid = false + } + } + + override fun getEditorInfo(): EditorInfo { + return editorInfo + } + + /** + * Runs code on the main thread, and blocks for the result on another. + * + * @param defaultResult the value to return if event timeout or if the connection is invalid. + * @param action the code to execute, that should return a result. + * @return the value produced by [action]. + */ + private fun <T> runOnMainAndAwaitResult(defaultResult: T, action: Callable<T>): T { + if (!inputConnectionValid) { + return defaultResult + } + if (Looper.myLooper() == Looper.getMainLooper()) { + return try { + action.call() + } catch (e: Exception) { + throw RuntimeException(e) + } + } + val futureTask = FutureTask(action) + handler.post(futureTask) + return try { + futureTask[ASYNC_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS] + } catch (e: ExecutionException) { + throw RuntimeException(e) + } catch (e: InterruptedException) { + defaultResult + } catch (e: TimeoutException) { + defaultResult + } + } + + /** + * Runs code on the main thread, and updates the [inputConnectionValid] with the result. + * + * @param action the code to execute, that should return a boolean indicating the validity of the + * connection. + * @return the value produced by [action] + */ + private fun runOnMainAndUpdateValidity(action: Callable<Boolean>): Boolean { + if (!inputConnectionValid) { + return false + } + + runOnMainDirect { + try { + inputConnectionValid = action.call() + } catch (ex: Exception) { + inputConnectionValid = false + throw RuntimeException("Input connection action failed", ex) + } + } + + return true + } + + /** + * Runs code on the main thread. Does not jump thread if already on the main thread. + * + * @param action the code to execute. + */ + private fun runOnMainDirect(action: Runnable) { + if (Looper.myLooper() == handler.looper) { + action.run() + } else { + handler.post(action) + } + } + + companion object { + private const val ASYNC_TIMEOUT_MILLIS: Long = 1000 + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/RendererCallback.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/RendererCallback.kt new file mode 100644 index 0000000..305ad38 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/RendererCallback.kt @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import android.os.Handler +import android.os.Looper +import android.view.inputmethod.EditorInfo +import androidx.car.app.CarContext +import androidx.car.app.activity.renderer.IProxyInputConnection +import androidx.car.app.activity.renderer.IRendererCallback +import androidx.car.app.utils.ThreadUtils +import androidx.lifecycle.Lifecycle +import com.android.car.libraries.apphost.CarHost +import com.android.car.libraries.apphost.logging.L +import com.android.car.libraries.apphost.logging.LogTags +import com.android.car.libraries.apphost.template.AppHost +import java.util.concurrent.Callable +import java.util.concurrent.ExecutionException +import java.util.concurrent.FutureTask +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +/** Handles events from the car app. */ +class RendererCallback(private val carHost: CarHost, private val inputManager: InputManagerImpl) : + IRendererCallback.Stub() { + private val handler = Handler(Looper.getMainLooper()) + + override fun onBackPressed() { + val appHost = carHost.getHostOrThrow(CarContext.APP_SERVICE) as? AppHost + appHost?.onBackPressed() + } + + override fun onCreate() { + ThreadUtils.runOnMain { carHost.dispatchAppLifecycleEvent(Lifecycle.Event.ON_CREATE) } + } + + override fun onStart() { + ThreadUtils.runOnMain { carHost.dispatchAppLifecycleEvent(Lifecycle.Event.ON_START) } + } + + override fun onResume() { + ThreadUtils.runOnMain { carHost.dispatchAppLifecycleEvent(Lifecycle.Event.ON_RESUME) } + } + + override fun onPause() { + ThreadUtils.runOnMain { + try { + carHost.dispatchAppLifecycleEvent(Lifecycle.Event.ON_PAUSE) + } catch (e: IllegalStateException) { + // Don't crash when dispatching on shutdown as you can run into race conditions. + } + } + } + + override fun onStop() { + ThreadUtils.runOnMain { + try { + carHost.dispatchAppLifecycleEvent(Lifecycle.Event.ON_STOP) + } catch (e: IllegalStateException) { + // Don't crash when dispatching on shutdown as you can run into race conditions. + } + } + } + + override fun onDestroyed() { + // Unlike the other lifecycle events, the fact that the CarAppActivity is destroyed does + // not mean that the CarAppBinding should be destroyed or unbound. We already have logic + // in CarHost to unbind the CarAppService after a specific timeout if the app remains in the + // STOPPED state (for non-nav apps). + } + + override fun onCreateInputConnection(editorInfo: EditorInfo): IProxyInputConnection? { + return runOnMainAndAwaitResult { inputManager.onCreateInputConnection(editorInfo) } + } + + /** + * Runs code on the main thread, and waits for the result. + * + * @param action the code to execute, that should return a result. + * @return the value produced by [action]. Returns null if times out or interrupted. + */ + private fun <T> runOnMainAndAwaitResult(action: Callable<T>): T? { + if (Looper.myLooper() == Looper.getMainLooper()) { + return try { + action.call() + } catch (e: Exception) { + throw RuntimeException(e) + } + } + val futureTask = FutureTask(action) + handler.post(futureTask) + return try { + futureTask[ASYNC_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS] + } catch (e: ExecutionException) { + throw RuntimeException(e) + } catch (e: InterruptedException) { + L.e(LogTags.APP_HOST, e, "Running call on main was interrupted.") + null + } catch (e: TimeoutException) { + L.e(LogTags.APP_HOST, e, "Running call on main was timed out.") + null + } + } + + companion object { + private const val ASYNC_TIMEOUT_MILLIS: Long = 1000 + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/RoutingInfoStateImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/RoutingInfoStateImpl.kt new file mode 100644 index 0000000..f696426 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/RoutingInfoStateImpl.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import com.android.car.libraries.apphost.common.RoutingInfoState + +/** Manages the state of routing information in template apps. */ +class RoutingInfoStateImpl : RoutingInfoState { + private var isVisible = false + + override fun setIsRoutingInfoVisible(isVisible: Boolean) { + this.isVisible = isVisible + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/StartCarAppUtil.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/StartCarAppUtil.kt new file mode 100644 index 0000000..5f9bf8b --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/StartCarAppUtil.kt @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import android.content.Context +import android.content.Intent +import androidx.annotation.VisibleForTesting +import androidx.car.app.CarContext +import androidx.car.app.activity.CarAppActivity +import com.android.car.libraries.apphost.NavigationIntentConverter +import com.google.common.base.Objects.equal +import java.lang.UnsupportedOperationException +import java.security.InvalidParameterException + +/** An utility class to validate calls to start a car app, and to perform them. */ +object StartCarAppUtil { + private const val PHONE_URI_PREFIX = "tel:" + + /** + * Metadata tag that points to the component name of the car app service that is linked to the car + * app service. + */ + @VisibleForTesting const val ACTIVITY_METADATA_KEY = "androidx.car.app.CAR_APP_ACTIVITY" + + /** + * Asserts that the `intent` follows the guidelines set in [CarContext.startCarApp] and starts the + * app. + * + * @param packageName the package name of the app that sent the intent + * @param intent the intent for starting the car app. + * @param allowedToStartSelf whether the calling app is allowed to start itself. Only nav apps + * ``` + * can call via [CarContext.startCarApp], and all apps can via a + * notification action. + * @throws SecurityException + * ``` + * if the app attempts to start a different app explicitly or + * ``` + * does not have permissions for the requested action. + * @throws InvalidParameterException + * ``` + * if the [Intent] does not meet the criteria listed at + * ``` + * [CarContext.startCarApp]. + * ``` + */ + fun validateStartCarAppIntent( + context: Context, + packageName: String, + intent: Intent, + allowedToStartSelf: Boolean + ): Intent { + val intentComponent = intent.component + val action = intent.action + + if (intentComponent != null && equal(intentComponent.packageName, packageName)) { + if (!allowedToStartSelf) { + throw SecurityException( + "The app is not a turn by turn navigation app, therefore it cannot start " + + "itself in the car" + ) + } + intent.setClassName(packageName, CarAppActivity::class.qualifiedName!!) + } else if (equal(action, CarContext.ACTION_NAVIGATE)) { + assertNavigationIntentIsValid(intent) + + // TODO(b/171308515): Add telemetry support. + } else if (equal(action, Intent.ACTION_DIAL) || equal(action, Intent.ACTION_CALL)) { + assertPhoneIntentIsValid(intent) + + // TODO(b/171308515): Add telemetry support. + } else if (intentComponent == null) { + throw InvalidParameterException("The intent is not for a supported action") + } else { + throw SecurityException("Explicitly starting a separate app is not supported") + } + + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + intent.resolveActivity(context.packageManager) + ?: throw UnsupportedOperationException( + "No component found to handle the startCarApp intent: $intent" + ) + + return intent + } + + /** + * Checks that the [Intent] is for a phone call by validating it meets the following: + * + * * The data is correctly formatted starting with "tel:"" + * * Has no component name set + */ + private fun assertPhoneIntentIsValid(intent: Intent) { + if (!intent.dataString.orEmpty().startsWith(PHONE_URI_PREFIX)) { + throw InvalidParameterException("Phone intent data is not properly formatted") + } + if (intent.component != null) { + throw SecurityException("Phone intent cannot have a component") + } + } + + /** + * Checks that the [Intent] is for navigation by validating it meets the following: + * + * * The data is formatted as described in [CarContext.startCarApp] + * * Has no component name set + */ + private fun assertNavigationIntentIsValid(intent: Intent) { + val uri = intent.data + if (uri == null || !equal(NavigationIntentConverter.GEO_QUERY_PREFIX, uri.scheme)) { + throw InvalidParameterException("Navigation intent has a malformed uri") + } + + val queryString = NavigationIntentConverter.getQueryString(uri) + if (queryString == null) { + if (NavigationIntentConverter.getCarLocation(uri) == null) { + throw InvalidParameterException( + "Navigation intent has neither a location nor a query string" + ) + } + } else { + if (uri.encodedSchemeSpecificPart.contains("daddr=")) { + // Other intent URIs support daddr, we do not as of right now. + throw InvalidParameterException( + "Navigation intent has neither latitude,longitude nor a query string" + ) + } + } + if (intent.component != null) { + throw SecurityException("Navigation intent cannot have a component") + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/StatusManager.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/StatusManager.kt new file mode 100644 index 0000000..563ddde --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/StatusManager.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import androidx.annotation.VisibleForTesting +import com.android.car.libraries.apphost.logging.StatusReporter +import java.io.PrintWriter +import java.util.SortedMap +import java.util.TreeMap + +/** Manager to handle collecting status information from components to be added to a bug report. */ +object StatusManager : StatusReporter { + /** Sections to include in the status information */ + enum class ReportSection { + APP_HOST, + SCREEN_RENDERES, + } + + private val statusReporters: SortedMap<ReportSection, StatusReporter> = TreeMap() + private val lock = Any() + + /** + * Adds a [StatusReporter] to be called for a bug report. + * + * @param section The section to be added to the bug report. + * @param reporter The [StatusReporter] that will fill in the information for the section. + */ + fun addStatusReporter(section: ReportSection, reporter: StatusReporter) { + synchronized(lock) { statusReporters.put(section, reporter) } + } + + /** + * Removes the [StatusReporter] for a given bug report section. + * + * @param section The section to remove, as passed to [.addStatusReporter]. + */ + fun removeStatusReporter(section: ReportSection) { + synchronized(lock) { statusReporters.remove(section) } + } + + @VisibleForTesting + fun clear() { + synchronized(lock) { statusReporters.clear() } + } + + override fun reportStatus(writer: PrintWriter, piiHandling: StatusReporter.Pii) { + synchronized(lock) { + for ((key, value) in statusReporters) { + writer.format("=== %s ===\n", key.name) + try { + value.reportStatus(writer, piiHandling) + } catch (throwable: Throwable) { + writer.format("\nError capturing dump for section: %s\n", throwable.message) + throwable.printStackTrace(writer) + } + writer.println() + } + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/SurfaceInfoProviderImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/SurfaceInfoProviderImpl.kt new file mode 100644 index 0000000..d7b696c --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/SurfaceInfoProviderImpl.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import android.graphics.Rect +import com.android.car.libraries.apphost.common.EventManager +import com.android.car.libraries.apphost.common.EventManager.EventType +import com.android.car.libraries.apphost.common.SurfaceInfoProvider + +/** Provides surface properties necessary for efficiently rendering partial content. */ +// TODO (b/206636788): Consolidate redundant code and improve documentation +internal class SurfaceInfoProviderImpl(private val eventManager: EventManager) : + SurfaceInfoProvider { + private var visibleArea: Rect? = null + private var stableArea: Rect? = null + + override fun getVisibleArea() = visibleArea + override fun getStableArea() = stableArea + + override fun setVisibleArea(area: Rect) { + val currentAreaNeedUpdated = visibleArea == null || area != visibleArea + visibleArea = area + if (currentAreaNeedUpdated) { + eventManager.dispatchEvent(EventType.SURFACE_VISIBLE_AREA) + } + + val stableAreaToUpdate = calculateStableArea(area, stableArea) + if (stableArea != stableAreaToUpdate) { + stableArea = stableAreaToUpdate + eventManager.dispatchEvent(EventType.SURFACE_STABLE_AREA) + } + } + + private fun calculateStableArea(visibleArea: Rect, stableArea: Rect?): Rect { + return if (stableArea == null || !stableArea.setIntersect(stableArea, visibleArea)) { + visibleArea + } else { + stableArea + } + } + + override fun invalidateStableArea() { + stableArea = null + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/TemplateContextImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/TemplateContextImpl.kt new file mode 100644 index 0000000..c0430fb --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/TemplateContextImpl.kt @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import android.content.ComponentName +import android.content.Context +import android.content.res.Configuration +import android.hardware.display.DisplayManager +import com.android.car.libraries.apphost.common.ANRHandler +import com.android.car.libraries.apphost.common.AppBindingStateProvider +import com.android.car.libraries.apphost.common.AppDispatcher +import com.android.car.libraries.apphost.common.BackPressedHandler +import com.android.car.libraries.apphost.common.CarAppError +import com.android.car.libraries.apphost.common.CarAppManager +import com.android.car.libraries.apphost.common.CarAppPackageInfo +import com.android.car.libraries.apphost.common.CarHostConfig +import com.android.car.libraries.apphost.common.ColorContrastCheckState +import com.android.car.libraries.apphost.common.ColorUtils +import com.android.car.libraries.apphost.common.DebugOverlayHandler +import com.android.car.libraries.apphost.common.ErrorHandler +import com.android.car.libraries.apphost.common.EventManager +import com.android.car.libraries.apphost.common.EventManager.EventType +import com.android.car.libraries.apphost.common.HostResourceIds +import com.android.car.libraries.apphost.common.RoutingInfoState +import com.android.car.libraries.apphost.common.StatusBarManager +import com.android.car.libraries.apphost.common.SurfaceCallbackHandler +import com.android.car.libraries.apphost.common.SurfaceInfoProvider +import com.android.car.libraries.apphost.common.SystemClockWrapper +import com.android.car.libraries.apphost.common.TemplateContext +import com.android.car.libraries.apphost.common.ToastController +import com.android.car.libraries.apphost.distraction.constraints.ConstraintsProvider +import com.android.car.libraries.apphost.input.InputConfig +import com.android.car.libraries.apphost.input.InputManager +import com.android.car.libraries.apphost.internal.ANRHandlerImpl +import com.android.car.libraries.apphost.internal.AppDispatcherImpl +import com.android.car.libraries.apphost.internal.CarAppPackageInfoImpl +import com.android.car.libraries.apphost.logging.TelemetryHandler +import com.android.car.libraries.templates.host.di.FeaturesConfig +import com.android.car.libraries.templates.host.di.HostApiLevelConfig +import com.android.car.libraries.templates.host.di.ThemeManager +import com.android.car.libraries.templates.host.di.UxreConfig +import java.io.PrintWriter + +/** A [TemplateContext] to provide to hosts and presenters. */ +class TemplateContextImpl +private constructor( + context: Context, + appName: ComponentName, + private val backPressedHandler: BackPressedHandler, + private val surfaceCallbackHandler: SurfaceCallbackHandler, + private val statusBarManager: StatusBarManager, + errorHandler: ErrorHandler, + private val toastController: ToastController, + private val displayId: Int, + private val inputManager: InputManager, + private val inputConfig: InputConfig, + private val carAppManager: CarAppManager, + private val telemetryHandler: TelemetryHandler, + private val debugOverlayHandler: DebugOverlayHandler, + private val routingInfoState: RoutingInfoState, + private val colorContrastCheckState: ColorContrastCheckState, + private val carHostConfig: CarHostConfig, + private val systemClockWrapper: SystemClockWrapper, + isNavigationApp: Boolean, + private val hostResourceIds: HostResourceIds, + uxreConfig: UxreConfig, + themeManager: ThemeManager +) : TemplateContext(context) { + + private val carAppPackageInfo: CarAppPackageInfo = + CarAppPackageInfoImpl.create( + context, + appName, + isNavigationApp, + hostResourceIds, + AppIconLoaderImpl + ) + private val eventManager: EventManager + private val surfaceInfoProvider: SurfaceInfoProvider + private val appDispatcher: AppDispatcher + private var appConfigurationContext: Context? = null + private var errorHandler: ErrorHandler + private val anrHandler: ANRHandler + private val constraintsProvider: ConstraintsProvider + private var lastError: CarAppError? = null + private val appBindingStateProvider: AppBindingStateProvider + + init { + this.errorHandler = + ErrorHandler { error -> + lastError = error + errorHandler.showError(error) + } + + themeManager.applyTheme(context) + + appBindingStateProvider = AppBindingStateProvider() + eventManager = EventManager() + anrHandler = ANRHandlerImpl.create(appName, errorHandler, telemetryHandler, eventManager) + constraintsProvider = ConstraintsProviderImpl(context, eventManager, uxreConfig) + surfaceInfoProvider = SurfaceInfoProviderImpl(eventManager) + appDispatcher = + AppDispatcherImpl.create( + appName, + errorHandler, + anrHandler, + telemetryHandler, + appBindingStateProvider + ) + + // Create a context configured with this context's configuration, the car display's display + // metrics, and the remote app's theme. + val packageContext = ColorUtils.getPackageContext(context, appName.packageName) + if (packageContext == null) { + appConfigurationContext = null + } else { + val configuration = resources.configuration + val display = context.getSystemService(DisplayManager::class.java).getDisplay(displayId) + appConfigurationContext = + packageContext.createDisplayContext(display).createConfigurationContext(configuration) + appConfigurationContext?.setTheme(ColorUtils.loadThemeId(context, appName)) + } + } + + override fun getErrorHandler() = errorHandler + override fun getAppConfigurationContext() = appConfigurationContext + override fun getStatusBarManager() = statusBarManager + override fun getInputManager() = inputManager + override fun getInputConfig() = inputConfig + override fun getCarAppPackageInfo() = carAppPackageInfo + override fun getBackPressedHandler() = backPressedHandler + override fun getSurfaceCallbackHandler() = surfaceCallbackHandler + override fun getSurfaceInfoProvider() = surfaceInfoProvider + override fun getEventManager() = eventManager + override fun getAnrHandler() = anrHandler + override fun getAppDispatcher() = appDispatcher + override fun getToastController() = toastController + override fun getCarAppManager() = carAppManager + override fun getTelemetryHandler() = telemetryHandler + override fun getDebugOverlayHandler() = debugOverlayHandler + override fun getHostResourceIds() = hostResourceIds + override fun getRoutingInfoState() = routingInfoState + override fun getColorContrastCheckState() = colorContrastCheckState + override fun getConstraintsProvider() = constraintsProvider + override fun getCarHostConfig() = carHostConfig + override fun getSystemClockWrapper() = systemClockWrapper + override fun getAppBindingStateProvider() = appBindingStateProvider + + override fun updateConfiguration(configuration: Configuration?) { + appConfigurationContext = + configuration?.let { appConfigurationContext?.createConfigurationContext(configuration) } + + // Propagate the configuration changed event to any listeners. + getEventManager().dispatchEvent(EventType.CONFIGURATION_CHANGED) + } + + override fun reportStatus(pw: PrintWriter) { + pw.printf("- app package info: %s\n", carAppPackageInfo) + pw.printf("- last error: %s\n", if (lastError != null) lastError else "n/a") + } + + companion object { + /** Creates a [TemplateContextImpl] for the car app identified by the given [ComponentName] */ + fun create( + context: Context, + appName: ComponentName, + displayId: Int, + backPressedHandler: BackPressedHandler, + surfaceCallbackHandler: SurfaceCallbackHandler, + statusBarManager: StatusBarManager, + debugOverlayHandler: DebugOverlayHandler, + inputManager: InputManager, + inputConfig: InputConfig, + carAppManager: CarAppManager, + isNavigationApp: Boolean, + hostResourceIds: HostResourceIds, + uxreConfig: UxreConfig, + hostApiLevelConfig: HostApiLevelConfig, + themeManager: ThemeManager, + telemetryHandler: TelemetryHandler, + featuresConfig: FeaturesConfig + ): TemplateContextImpl { + return TemplateContextImpl( + context, + appName, + backPressedHandler, + surfaceCallbackHandler, + statusBarManager, + ErrorHandlerImpl.create(context, appName, carAppManager, hostResourceIds), + ToastControllerImpl(context), + displayId, + inputManager, + inputConfig, + carAppManager, + telemetryHandler, + debugOverlayHandler, + RoutingInfoStateImpl(), + ColorContrastCheckStateImpl(), + CarHostConfigImpl(context, appName, hostApiLevelConfig, featuresConfig), + SystemClockWrapper(), + isNavigationApp, + hostResourceIds, + uxreConfig, + themeManager + ) + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ToastControllerImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ToastControllerImpl.kt new file mode 100644 index 0000000..57f0328 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ToastControllerImpl.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal + +import android.content.Context +import android.widget.Toast +import com.android.car.libraries.apphost.common.ToastController + +/** Manages the toasts on car screen. */ +class ToastControllerImpl(private val context: Context) : ToastController { + override fun showToast(text: CharSequence?, duration: Int) { + Toast.makeText(context, text, duration).show() + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/AndroidManifest.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/AndroidManifest.xml new file mode 100644 index 0000000..16dafa5 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/AndroidManifest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + Copyright (C) 2021 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 + + https://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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.car.libraries.templates.host.internal.debug"> + + <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="31" /> + + <application> + <activity + android:name=".ClusterActivity" + android:allowEmbedded="true" + android:excludeFromRecents="true" + android:exported="true" + android:launchMode="singleInstance" + android:process=":renderer_service" + android:resizeableActivity="true" + android:screenOrientation="user" + android:theme="@style/Theme.Template"> + <!-- In car_embedded builds, indicate that we are distraction optimized to prevent maps + from being killed when the car is moving. --> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.car.cluster.NAVIGATION" /> + </intent-filter> + + <meta-data + android:name="distractionOptimized" + android:value="true" /> + </activity> + </application> +</manifest> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/ClusterActivity.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/ClusterActivity.kt new file mode 100644 index 0000000..c5a082c --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/ClusterActivity.kt @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.internal.debug + +import android.annotation.SuppressLint +import android.car.Car +import android.content.Intent +import android.graphics.Color +import android.graphics.Rect +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import androidx.appcompat.app.AppCompatActivity +import android.view.View +import android.view.ViewGroup +import androidx.annotation.StyleableRes +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.android.car.libraries.apphost.common.TemplateContext +import com.android.car.libraries.apphost.distraction.constraints.CarColorConstraints +import com.android.car.libraries.apphost.logging.L +import com.android.car.libraries.apphost.logging.LogTags +import com.android.car.libraries.apphost.view.common.CarTextParams +import com.android.car.libraries.templates.host.internal.HostNavState +import com.android.car.libraries.templates.host.internal.NavigationCoordinator +import com.android.car.libraries.templates.host.view.widgets.navigation.CompactStepView +import com.android.car.libraries.templates.host.view.widgets.navigation.DetailedStepView +import com.android.car.libraries.templates.host.view.widgets.navigation.ProgressView +import com.android.car.libraries.templates.host.view.widgets.navigation.TravelEstimateView +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import com.android.car.libraries.templates.host.R + +/** + * This activity will be launched by the system to show the user navigation updates in the + * instrument cluster. + */ +class ClusterActivity : AppCompatActivity() { + private lateinit var root: ViewGroup + + // travel estimate card will only be enabled if there's enough room on screen + private var travelEstimateEnabled = false + private var travelEstimateView: TravelEstimateView? = null + private var travelEstimateContainer: ViewGroup? = null + private lateinit var detailedStepView: DetailedStepView + private lateinit var compactStepView: CompactStepView + private lateinit var progressView: ProgressView + + private var carTextParams: CarTextParams? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.cluster_activity) + root = findViewById(R.id.root) + travelEstimateContainer = findViewById(R.id.travel_estimate_card_container) + travelEstimateView = findViewById(R.id.travel_estimate_view) + detailedStepView = findViewById(R.id.detailed_step_view) + compactStepView = findViewById(R.id.compact_step_view) + progressView = findViewById(R.id.progress_view) + + initColors() + // `root` hasn't finished measuring yet, and will report width=0, so we need to throw this work + // to end of the MainLooper's queue. + Handler(Looper.getMainLooper()).post { + adjustViewport(intent) + calcTravelEstimateEnabled() + } + + observeNavigationState() + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + adjustViewport(intent) + } + + private fun initColors() { + // Read the fallback color to use with the app-defined card background color. + @StyleableRes + val themeAttrs = + intArrayOf( + com.android.car.libraries.templates.host.R.attr.templateNavCardFallbackContentColor + ) + val ta = obtainStyledAttributes(themeAttrs) + val contentColor = ta.getColor(0, Color.WHITE) + ta.recycle() + detailedStepView.setTextColor(contentColor) + compactStepView.setTextColor(contentColor) + progressView.setColor(contentColor) + } + + private fun observeNavigationState() { + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + NavigationCoordinator.getInstance(applicationContext).navigationState.collect { state -> + when (state) { + HostNavState.NotNavigating -> { + detailedStepView.setStepAndDistance(null, null, null, null, Color.TRANSPARENT, false) + compactStepView.setStep(null, null, null, Color.TRANSPARENT) + travelEstimateContainer?.visibility = View.GONE + } + is HostNavState.Navigating -> { + renderTrip(state) + } + } + } + } + } + } + + private fun renderTrip(state: HostNavState.Navigating) { + val trip = state.trip + val templateContext = state.templateContext + val step = trip.steps.firstOrNull() + val travelEstimate = trip.stepTravelEstimates.firstOrNull() + val nextStep = trip.steps.elementAtOrNull(1) + val carTextParams = + carTextParams ?: createStepTextParams(templateContext).also { carTextParams = it } + detailedStepView.setStepAndDistance( + templateContext, + step, + travelEstimate?.remainingDistance, + carTextParams, + Color.TRANSPARENT, + false + ) + compactStepView.setStep(templateContext, nextStep, carTextParams, Color.TRANSPARENT) + + progressView.visibility = if (trip.isLoading) View.VISIBLE else View.GONE + if (travelEstimateEnabled && travelEstimate != null) { + travelEstimateContainer?.visibility = View.VISIBLE + travelEstimateView?.setTravelEstimate(templateContext, travelEstimate) + } else { + travelEstimateContainer?.visibility = View.GONE + } + } + + /** + * Some of the display might be obscured by either the shape of the physical screen, or other + * elements in the cluster display. We need to respect this constraint and only display our UI + * within those bounds. + */ + private fun adjustViewport(intent: Intent?) { + intent ?: return + val bundle = intent.getBundleExtra(Car.CAR_EXTRA_CLUSTER_ACTIVITY_STATE) ?: return + val viewport = bundle.getParcelable<Rect>("android.car:activityState.unobscured") ?: return + + L.d(LogTags.CLUSTER) { "cluster un-obscured area: $viewport" } + root.setPadding( + viewport.left, + viewport.top, + root.width - viewport.right, + root.height - viewport.bottom + ) + } + + private fun calcTravelEstimateEnabled() { + val top = root.top + root.paddingTop + val bottom = root.bottom - root.paddingBottom + val safeAreaHeight = bottom - top + val threshold = + resources.getDimensionPixelSize(R.dimen.travel_estimate_card_min_height_threshold) + travelEstimateEnabled = safeAreaHeight > threshold + } + + /** + * Returns a [CarTextParams] instance to use for the text of a step. + * + * Unlike other text elsewhere, image spans are allowed in these strings. + */ + @SuppressLint("ResourceType") + private fun createStepTextParams(templateContext: TemplateContext): CarTextParams? { + @StyleableRes + val themeAttrs = + intArrayOf( + R.attr.templateRoutingImageSpanRatio, + R.attr.templateRoutingImageSpanBody2MaxHeight, + R.attr.templateRoutingImageSpanBody3MaxHeight + ) + val ta = templateContext.obtainStyledAttributes(themeAttrs) + val imageRatio = ta.getFloat(0, 0f) + val body2MaxHeight = ta.getDimensionPixelSize(1, 0) + ta.recycle() + val maxWidth = (body2MaxHeight * imageRatio).toInt() + return CarTextParams.builder() + .setImageBoundingBox(Rect(0, 0, maxWidth, body2MaxHeight)) + .setMaxImages(2) + .setColorSpanConstraints(CarColorConstraints.NO_COLOR) + .build() + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/layout/cluster_activity.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/layout/cluster_activity.xml new file mode 100644 index 0000000..46b2c01 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/layout/cluster_activity.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + Copyright (C) 2021 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 + + https://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" + android:id="@+id/root" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <include + android:id="@+id/steps_card_container" + layout="@layout/steps_card_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="?templateCardContentContainerBottomMargin" + app:layout_constraintBottom_toTopOf="@id/travel_estimate_card_container" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_chainStyle="packed" /> + + <include + android:id="@+id/travel_estimate_card_container" + layout="@layout/travel_estimate_card_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/steps_card_container" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/styles/res/values/dimens.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/styles/res/values/dimens.xml new file mode 100644 index 0000000..234d34f --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/styles/res/values/dimens.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + Copyright (C) 2021 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 + + https://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> + <!-- Travel estimate card should only be shown if the total safe area available for cluster is larger than this threshold --> +<!-- <dimen name="travel_estimate_card_min_height_threshold">400dp</dimen>--> +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/values/dimens.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/values/dimens.xml new file mode 100644 index 0000000..bdee464 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/values/dimens.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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> + <dimen name="car_app_ui_cluster_nav_icon_size">48dp</dimen> + <dimen name="car_app_ui_cluster_nav_text_size">40sp</dimen> + <!-- Travel estimate card should only be shown if the total safe area available for cluster is larger than this threshold --> + <dimen name="travel_estimate_card_min_height_threshold">400dp</dimen> +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/values/strings.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/values/strings.xml new file mode 100644 index 0000000..248e56b --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/values/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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> + <!-- Cluster serves drivers. They don't need contentDescription. --> + <string name="dummy_content_description" translatable="false" /> +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/values/styles.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/values/styles.xml new file mode 100644 index 0000000..a90d718 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/values/styles.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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="Theme.AppCompat.NoActionBar.Fullscreen" parent="Theme.AppCompat.NoActionBar"> + <item name="android:windowNoTitle">true</item> + <item name="android:windowActionBar">false</item> + <item name="android:windowFullscreen">true</item> + <item name="android:windowContentOverlay">@null</item> + </style> +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/res/values/bools.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/res/values/bools.xml new file mode 100644 index 0000000..8d949aa --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/res/values/bools.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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> + <!-- Whether or not NavState should be sent to NavigationManager --> + <bool name="send_navstates_to_system">true</bool> +</resources>
\ No newline at end of file diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/res/values/integers.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/res/values/integers.xml new file mode 100644 index 0000000..93f023f --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/res/values/integers.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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> + + <!-- How long to wait before unbinding from the service after the user leaves an app. --> + <integer name="app_unbind_delay_seconds">180</integer> + + <!-- The max length of a car app list for showing routes. --> + <integer name="route_list_max_length">3</integer> + + <!-- The max length of a car app list for showing pane information. --> + <integer name="pane_max_length">4</integer> + + <!-- The max size for the template stack for the car app. --> + <integer name="template_stack_max_size">5</integer> + + <!-- Default max string length --> + <integer name="car_app_default_max_string_length">120</integer> + + <!-- How long to keep Cluster Icons in memory --> + <integer name="cluster_icon_cache_duration_millis">10000</integer> + + <!-- How long to wait for Trip conversion before giving up on this update --> + <integer name="cluster_trip_to_navstate_conversion_timeout_millis">1000</integer> +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_action_button_background_color_selector.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_action_button_background_color_selector.xml new file mode 100644 index 0000000..9707746 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_action_button_background_color_selector.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + https://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. +--> +<selector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <!-- Primary color --> + <item app:type_primary="true" + android:color="@color/car_app_ui_action_button_primary_background_color"/> + <!-- Default--> + <item android:color="@color/car_app_ui_action_button_default_background_color"/> +</selector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_background_color_selector.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_background_color_selector.xml new file mode 100644 index 0000000..ee4c7a0 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_background_color_selector.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + + <!-- Disabled state--> + <item android:state_enabled="false" android:color="@color/default_gray_928"/> + + <!-- Default--> + <item android:color="@color/default_gray_868"/> +</selector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_color_selector.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_color_selector.xml new file mode 100644 index 0000000..e2e0aad --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_color_selector.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + + <!-- Disabled state--> + <item android:state_enabled="false" android:color="@color/default_gradient_white_56"/> + + <!-- Default--> + <item android:color="@color/default_white"/> +</selector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_foreground_color_selector.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_foreground_color_selector.xml new file mode 100644 index 0000000..fc38e92 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_foreground_color_selector.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <!-- Note: order of the following lines is important --> + <item app:state_error="true" android:color="@color/car_app_ui_edit_text_error_color"/> + <item android:state_focused="true" android:color="@color/car_app_ui_edit_text_active_color"/> + <item android:state_enabled="true" android:color="@color/car_app_ui_edit_text_enabled_color"/> + <item android:color="@color/car_app_ui_edit_text_disabled_color"/> +</selector>
\ No newline at end of file diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_hint_color_selector.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_hint_color_selector.xml new file mode 100644 index 0000000..d2e2056 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_hint_color_selector.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + + <!-- Disabled state--> + <item android:state_enabled="false" android:color="@color/default_gradient_white_56"/> + + <!-- Default--> + <item android:color="@color/default_gradient_white_72"/> +</selector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_ripple_color_selector.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_ripple_color_selector.xml new file mode 100644 index 0000000..c6c3432 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_ripple_color_selector.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + https://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. +--> +<!-- Due to b/120995067 in order to keep normal ripple effect in touch, the color selector must show +for all states. In rotary and touchpad, per go/aa-focus-design, don't draw ripple when not pressed. +Doing so also avoids "ghost" effect when rapidly moving focus across Views. --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="@color/default_controller_ripple_selector_color"/> +</selector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_action_button_background.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_action_button_background.xml new file mode 100644 index 0000000..4aaee56 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_action_button_background.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- The background fill --> + <item> + <shape android:shape="rectangle"> + <solid android:color="@color/default_action_button_background_color_selector"/> + <corners + android:radius="@dimen/car_app_ui_button_corner_radius"/> + </shape> + </item> + + <!-- Masked ripple layer --> + <item android:drawable="@drawable/default_action_button_ripple"/> +</layer-list> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_action_button_ripple.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_action_button_ripple.xml new file mode 100644 index 0000000..ba31afc --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_action_button_ripple.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/default_ripple_color_selector"> + <!-- Ripple layer masked so that it is only drawn within the bounds of the view --> + <item + android:id="@android:id/mask" + android:gravity="center"> + <shape android:shape="rectangle"> + <solid android:color="@color/default_ripple_color_selector"/> + <corners android:radius="@dimen/car_app_ui_button_corner_radius"/> + </shape> + </item> +</ripple> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_edit_text_background.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_edit_text_background.xml new file mode 100644 index 0000000..e6b3670 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_edit_text_background.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <shape android:shape="rectangle"> + <solid android:color="@color/default_edit_text_background_color_selector"/> + <corners android:radius="@dimen/car_app_ui_corner_radius"/> + </shape> + </item> +</layer-list> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_edit_text_foreground.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_edit_text_foreground.xml new file mode 100644 index 0000000..4451f8d --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_edit_text_foreground.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:gravity="bottom"> + <shape> + <size android:height="@dimen/car_app_ui_edit_text_border_width" /> + <solid android:color="@color/default_edit_text_foreground_color_selector" /> + </shape> + </item> +</layer-list> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_ic_pan_button.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_ic_pan_button.xml new file mode 100644 index 0000000..171d5c2 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_ic_pan_button.xml @@ -0,0 +1,28 @@ +<!-- + Copyright (C) 2021 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 + + https://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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M15.54,5.54L13.77,7.3 12,5.54 10.23,7.3 8.46,5.54 12,2zM18.46,15.54l-1.76,-1.77L18.46,12l-1.76,-1.77 1.76,-1.77L22,12zM8.46,18.46l1.77,-1.76L12,18.46l1.77,-1.76 1.77,1.76L12,22zM5.54,8.46l1.76,1.77L5.54,12l1.76,1.77 -1.76,1.77L2,12z"/> + <path + android:fillColor="@android:color/white" + android:pathData="M12,12m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"/> +</vector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_ic_refresh_button.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_ic_refresh_button.xml new file mode 100644 index 0000000..6384396 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_ic_refresh_button.xml @@ -0,0 +1,25 @@ +<!-- + Copyright (C) 2021 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 + + https://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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M12,20q-3.35,0 -5.675,-2.325Q4,15.35 4,12q0,-3.35 2.325,-5.675Q8.65,4 12,4q1.725,0 3.3,0.713 1.575,0.712 2.7,2.037V4h2v7h-7V9h4.2q-0.8,-1.4 -2.188,-2.2Q13.625,6 12,6 9.5,6 7.75,7.75T6,12q0,2.5 1.75,4.25T12,18q1.925,0 3.475,-1.1T17.65,14h2.1q-0.7,2.65 -2.85,4.325Q14.75,20 12,20z"/> +</vector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values-night/colors.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values-night/colors.xml new file mode 100644 index 0000000..24bad14 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values-night/colors.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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="default_hun_text_color">#E0FFFFFF</color> + <color name="default_hun_text_color2">#80FFFFFF</color> + + <color name="default_card_text_color">#CCFFFFFF</color> + <color name="default_card_background_color">@color/default_gray_868</color> + <color name="default_focus_blue">#2371CD</color> + +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values-night/colors_overlayable.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values-night/colors_overlayable.xml new file mode 100644 index 0000000..f7a1c51 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values-night/colors_overlayable.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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> + + <!-- Buttons. --> + <color name="car_app_ui_floating_button_default_background_color">@color/default_white</color> + <color name="car_app_ui_floating_button_default_text_color">@color/default_black</color> + +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/attrs.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/attrs.xml new file mode 100644 index 0000000..07b3c19 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/attrs.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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> + <!-- Custom error state to be used in edit boxes or other components that support this state --> +<!-- <declare-styleable name="ErrorState">--> +<!-- <attr name="state_error" format="boolean"/>--> +<!-- </declare-styleable>--> + + <!-- Custom button type to be used in action buttons or other component that support this + classification --> + <declare-styleable name="ButtonType"> + <!-- Indicates this a "primary" button, out of a set of other buttons --> + <attr name="type_primary" format="boolean"/> + <!-- Indicates this an app button, background color controlled by the app --> + <attr name="type_custom" format="boolean"/> + </declare-styleable> +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/bools_overlayable.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/bools_overlayable.xml new file mode 100644 index 0000000..22e64fe --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/bools_overlayable.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!-- LINT.IfChange --> +<resources> + <!-- Boolean definitions that can be overridden by the OEMs. + + !!! IMPORTANT !!! + Do not refer to these booleans directly from views. Booleans must be + referred to through theme attributes (in attrs.xml). + + Any new resource added to this file should also be added to the + overlayable.xml before OEMs can customize them. + + !!! IMPORTANT !!! + These resources must be added to the overlayable.xml file. DO NOT + add documentation here. Concentrate all documentation in public-ready + comments in the overlayable.xml file. --> + + <bool name="car_app_ui_customized">false</bool> + <bool name="car_app_ui_is_action_color_overridden">false</bool> + <bool name="car_app_ui_action_button_list_button_stretch_horizontal">false</bool> +</resources> +<!-- LINT.ThenChange(overlayable.xml) --> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/colors.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/colors.xml new file mode 100644 index 0000000..0ca7b10 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/colors.xml @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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> + <!-- Colors used as the default value for overlayable resources. + These resources are not directly overlayable. --> + <color name="default_white">#FFFFFF</color> + <color name="default_gradient_white_12">#1FFFFFFF</color> + <color name="default_gradient_white_16">#29FFFFFF</color> + <color name="default_gradient_white_24">#3DFFFFFF</color> + <color name="default_gradient_white_40">#66FFFFFF</color> + <color name="default_gradient_white_46">#75FFFFFF</color> + <color name="default_gradient_white_56">#8FFFFFFF</color> + <color name="default_gradient_white_72">#B8FFFFFF</color> + <color name="default_gray_50">#F8F9FA</color> + <color name="default_gray_100">#F1F3F4</color> + <color name="default_gray_200">#E8EAED</color> + <color name="default_gray_300">#DADCE0</color> + <color name="default_gray_400">#BDC1C6</color> + <color name="default_gray_500">#9AA0A6</color> + <color name="default_gray_600">#80868B</color> + <color name="default_gray_700">#5F6368</color> + <color name="default_gray_800">#3C4043</color> + <color name="default_gray_846">#2E3134</color> + <color name="default_gray_868">#282A2D</color> + <color name="default_gray_878">#2A2A29</color> + <color name="default_gray_900">#202124</color> + <color name="default_gray_928">#17181B</color> + <color name="default_gray_958">#0E1013</color> + <color name="default_black">#000000</color> + <color name="default_gradient_black_0">#00000000</color> + <color name="default_gradient_black_25">#40000000</color> + <color name="default_gradient_black_64">#A3000000</color> + <color name="default_gradient_black_72">#B8000000</color> + <color name="default_gradient_black_85">#D9000000</color> + <color name="default_gradient_black_88">#E0000000</color> + <color name="default_gradient_black_100">#FF000000</color> + + <!-- Default colors. --> + <color name="default_text_color">@color/default_white</color> + + <!-- Standard colors --> + <color name="default_standard_red">#FFEE675C</color> + <color name="default_standard_red_dark">#FFC5221F</color> + <color name="default_standard_green">#FF61AC70</color> + <color name="default_standard_green_dark">#FF448B47</color> + <color name="default_standard_blue">#FF669DF6</color> + <color name="default_standard_blue_dark">#FF3674E0</color> + <color name="default_standard_yellow">#FFE9A240</color> + <color name="default_standard_yellow_dark">#FFD5792D</color> + + <!-- Default car app colors is customizable only with Car UI Library. --> + <color name="default_primary_color">@color/car_ui_text_color_primary</color> + <color name="default_primary_dark_color">@color/car_ui_text_color_primary</color> + <color name="default_secondary_color">@color/car_ui_text_color_secondary</color> + <color name="default_secondary_dark_color">@color/car_ui_text_color_secondary</color> + + <!-- LINT.IfChange --> + <color name="default_hun_text_color">@color/default_white</color> + <color name="default_hun_text_color2">#8FFFFFFF</color> + + <color name="default_card_text_color">@color/default_white</color> + <color name="default_background_color">@color/default_black</color> + <color name="default_card_background_color">@color/default_gray_846</color> + <color name="default_focus_blue">#4B9EFF</color> + <!-- LINT.ThenChange(../values-night/colors.xml) --> + + <color name="default_message_debug_text_color">#FF57F1B1</color> + + <color name="default_focus_no_content">#48FFFFFF</color> + <color name="default_controller_ripple_selector_color">#b27da9c7</color> + <color name="default_controller_ripple_color">#66ffffff</color> +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/colors_overlayable.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/colors_overlayable.xml new file mode 100644 index 0000000..e9c2531 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/colors_overlayable.xml @@ -0,0 +1,72 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!-- LINT.IfChange --> +<resources> + <!-- Color definitions that can be overridden by the OEMs. + + !!! IMPORTANT !!! + Do not refer to these colors directly from views. Colors must be referred + to through theme attributes (in attrs.xml). + + Any new resource added to this file should also be added to the + overlayable.xml before OEMs can customize them. + + !!! IMPORTANT !!! + These resources must be added to the overlayable.xml file. DO NOT + add documentation here. Concentrate all documentation in public-ready + comments in the overlayable.xml file. + --> + + <!-- Standard colores --> + <color name="car_app_ui_standard_red">@color/default_standard_red</color> + <color name="car_app_ui_standard_red_dark">@color/default_standard_red_dark</color> + <color name="car_app_ui_standard_green">@color/default_standard_green</color> + <color name="car_app_ui_standard_green_dark">@color/default_standard_green_dark</color> + <color name="car_app_ui_standard_blue">@color/default_standard_blue</color> + <color name="car_app_ui_standard_blue_dark">@color/default_standard_blue_dark</color> + <color name="car_app_ui_standard_yellow">@color/default_standard_yellow</color> + <color name="car_app_ui_standard_yellow_dark">@color/default_standard_yellow_dark</color> + + <!-- Button --> + <color name="car_app_ui_action_button_default_background_color">@color/default_gray_846</color> + <color name="car_app_ui_action_button_primary_background_color">@color/default_standard_blue</color> + <color name="car_app_ui_action_button_text_color">@color/default_white</color> + <color name="car_app_ui_floating_button_default_background_color">@color/default_black</color> + <color name="car_app_ui_floating_button_default_text_color">@color/default_white</color> + + <!-- Read-only Text --> + <color name="car_app_ui_read_only_text_color">@color/default_black</color> + <color name="car_app_ui_read_only_text_background_color">@color/default_white</color> + + <!-- Edit Text --> + <color name="car_app_ui_edit_text_active_color">@color/car_app_ui_standard_blue</color> + <color name="car_app_ui_edit_text_enabled_color">@color/default_gradient_white_72</color> + <color name="car_app_ui_edit_text_error_color">@color/car_app_ui_standard_red</color> + <color name="car_app_ui_edit_text_disabled_color">@color/default_gradient_white_56</color> + + <!-- Hyperlink Text --> + <color name="car_app_ui_hyperlink_text_color">@color/default_white</color> + + <!-- Rows --> + <color name="car_app_ui_row_background_color">@color/car_ui_activity_background_color</color> + + <!-- Grids --> + <color name="car_app_ui_grid_item_background_color">@color/car_ui_activity_background_color</color> + +</resources> +<!-- LINT.ThenChange(overlayable.xml) --> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/dimens_overlayable.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/dimens_overlayable.xml new file mode 100644 index 0000000..dd56d1e --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/dimens_overlayable.xml @@ -0,0 +1,116 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!-- LINT.IfChange --> +<resources> + <!-- Dimension definitions that can be overridden by the OEMs. + + !!! IMPORTANT !!! + Do not refer to these dimensions directly from views. Dimensions must be + referred to through theme attributes (in attrs.xml). + + Any new resource added to this file should also be added to the + overlayable.xml before OEMs can customize them. + + !!! IMPORTANT !!! + These resources must be added to the overlayable.xml file. DO NOT + add documentation here. Concentrate all documentation in public-ready + comments in the overlayable.xml file. --> + + <!-- Template element spacing --> + <dimen name="car_app_ui_image_to_text_spacing_vertical">@dimen/car_ui_padding_4</dimen> + <dimen name="car_app_ui_text_to_control_spacing_vertical">@dimen/car_ui_padding_5</dimen> + <dimen name="car_app_ui_text_to_secondary_control_spacing_vertical">@dimen/car_ui_padding_7</dimen> + <dimen name="car_app_ui_control_to_text_spacing_vertical">@dimen/car_ui_padding_4</dimen> + <dimen name="car_app_ui_control_to_control_spacing_horizontal">@dimen/car_ui_padding_3</dimen> + <dimen name="car_app_ui_content_horizontal_margin">24dp</dimen> + <dimen name="car_app_ui_touch_target_size">@dimen/car_ui_touch_target_size</dimen> + + <!-- Template element corner radius --> + <dimen name="car_app_ui_corner_radius">8dp</dimen> + + <!-- Card spacing --> + <dimen name="car_app_ui_card_start_margin">@dimen/car_ui_padding_3</dimen> + <dimen name="car_app_ui_card_top_margin">@dimen/car_ui_padding_3</dimen> + <dimen name="car_app_ui_card_width">0dp</dimen> + + <!-- Template image sizing --> + <dimen name="car_app_ui_large_image_size">@dimen/car_ui_list_item_content_icon_width</dimen> + + <!-- Navigation card spacing --> + <dimen name="car_app_ui_nav_card_padding_vertical">@dimen/car_ui_padding_3</dimen> + <dimen name="car_app_ui_nav_card_padding_horizontal">@dimen/car_ui_padding_3</dimen> + <dimen name="car_app_ui_nav_card_image_to_text_spacing_horizontal">@dimen/car_ui_padding_3</dimen> + <dimen name="car_app_ui_nav_card_large_text_size">32sp</dimen> + <dimen name="car_app_ui_nav_card_xlarge_text_size">44sp</dimen> + <dimen name="car_app_ui_nav_card_small_padding_vertical">@dimen/car_ui_padding_1</dimen> + <dimen name="car_app_ui_nav_card_image_to_text_spacing_vertical">@dimen/car_ui_padding_1</dimen> + <dimen name="car_app_ui_nav_card_width">0dp</dimen> + <dimen name="car_app_ui_nav_card_small_image_size">36dp</dimen> + <dimen name="car_app_ui_nav_card_large_image_size">64dp</dimen> + + <!-- Card header spacing/sizing --> + <dimen name="car_app_ui_card_header_image_size">44dp</dimen> + <dimen name="car_app_ui_card_header_text_padding_horizontal">@dimen/car_ui_padding_1</dimen> + <dimen name="car_app_ui_card_header_text_padding_vertical">@dimen/car_ui_padding_2</dimen> + <dimen name="car_app_ui_card_header_no_button_text_margin_start">@dimen/car_ui_padding_4</dimen> + + <!-- Grid item spacing/sizing --> + <dimen name="car_app_ui_grid_item_vertical_spacing">@dimen/car_ui_padding_3</dimen> + <dimen name="car_app_ui_grid_item_image_to_text_spacing_vertical">@dimen/car_ui_padding_1</dimen> + <dimen name="car_app_ui_grid_item_text_to_text_spacing_vertical">@dimen/car_ui_padding_1</dimen> + + <!-- Button spacing/sizing --> + <dimen name="car_app_ui_button_height">56dp</dimen> + <dimen name="car_app_ui_button_image_size">36dp</dimen> + <dimen name="car_app_ui_icon_button_start_spacing">@dimen/car_ui_padding_3</dimen> + <dimen name="car_app_ui_icon_button_end_spacing">@dimen/car_ui_padding_4</dimen> + <dimen name="car_app_ui_icon_button_image_to_text_spacing">@dimen/car_ui_padding_1</dimen> + <dimen name="car_app_ui_button_text_horizontal_spacing">@dimen/car_ui_padding_5</dimen> + <dimen name="car_app_ui_button_corner_radius">4dp</dimen> + <dimen name="car_app_ui_action_button_list_button_max_width">800dp</dimen> + <dimen name="car_app_ui_button_side_alignment_spacing">@dimen/car_ui_padding_4</dimen> + + <!-- Edit text spacing/sizing --> + <dimen name="car_app_ui_edit_text_top_padding">@dimen/car_ui_padding_4</dimen> + <dimen name="car_app_ui_edit_text_bottom_padding">@dimen/car_ui_padding_4</dimen> + <dimen name="car_app_ui_edit_text_start_padding">@dimen/car_ui_padding_2</dimen> + <dimen name="car_app_ui_edit_text_end_padding">@dimen/car_ui_padding_4</dimen> + <dimen name="car_app_ui_edit_text_error_vertical_spacing">@dimen/car_ui_padding_1</dimen> + <dimen name="car_app_ui_edit_text_error_horizontal_spacing">@dimen/car_ui_padding_3</dimen> + <dimen name="car_app_ui_edit_text_border_width">2dp</dimen> + + <!-- Read-only Text. --> + <dimen name="car_app_ui_read_only_text_padding">@dimen/car_ui_padding_4</dimen> + + <!-- Compact row spacing/sizing. These rows are used inside cards. --> + <dimen name="car_app_ui_half_row_min_height">0dp</dimen> + <dimen name="car_app_ui_half_row_horizontal_padding">@dimen/car_ui_padding_3</dimen> + <dimen name="car_app_ui_half_row_vertical_padding">@dimen/car_ui_padding_2</dimen> + <dimen name="car_app_ui_half_row_image_to_text_spacing">@dimen/car_ui_padding_3</dimen> + <dimen name="car_app_ui_half_row_text_to_text_spacing">@dimen/car_ui_padding_0</dimen> + <dimen name="car_app_ui_half_row_image_size">44dp</dimen> + + <!-- Full row spacing/sizing. --> + <dimen name="car_app_ui_full_row_start_padding">@dimen/car_ui_list_item_text_start_margin</dimen> + <dimen name="car_app_ui_full_row_end_padding">0dp</dimen> + + <!-- Sign-in template spacing/sizing. --> + <dimen name="car_app_ui_sign_in_method_max_width">640dp</dimen> + +</resources> +<!-- LINT.ThenChange(overlayable.xml) --> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/drawable.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/drawable.xml new file mode 100644 index 0000000..f363b45 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/drawable.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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> + <!-- Drawables used as the default value for overlayable resources. + These resources are not directly overlayable. --> + <drawable name="default_error_icon">@drawable/car_ui_icon_error</drawable> + <drawable name="default_alert_icon">@drawable/car_ui_icon_error</drawable> + <drawable name="default_back_icon">@drawable/car_ui_icon_arrow_back</drawable> +</resources>
\ No newline at end of file diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/drawable_overlayable.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/drawable_overlayable.xml new file mode 100644 index 0000000..042bbf4 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/drawable_overlayable.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!-- LINT.IfChange --> +<resources> + <!-- Drawable definitions that can be overridden by the OEMs. + + !!! IMPORTANT !!! + Do not refer to these drawables directly from views. Drawables must be referred + to through theme attributes (in attrs.xml). + + Any new resource added to this file should also be added to the + overlayable.xml before OEMs can customize them. + + !!! IMPORTANT !!! + These resources must be added to the overlayable.xml file. DO NOT + add documentation here. Concentrate all documentation in public-ready + comments in the overlayable.xml file. --> + + <drawable name="car_app_ui_action_button_background">@drawable/default_action_button_background</drawable> +</resources> +<!-- LINT.ThenChange(overlayable.xml) --> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/integers.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/integers.xml new file mode 100644 index 0000000..0a589d5 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/integers.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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> + <!-- Gravity integer values (to be used as part of gravity overlayable attributes. --> +<!-- <integer name="gravity_bottom">80</integer>--> +<!-- <integer name="gravity_center">17</integer>--> +<!-- <integer name="gravity_center_horizontal">1</integer>--> +<!-- <integer name="gravity_center_vertical">16</integer>--> +<!-- <integer name="gravity_end">8388613</integer>--> +<!-- <integer name="gravity_left">3</integer>--> +<!-- <integer name="gravity_no_gravity">0</integer>--> +<!-- <integer name="gravity_right">5</integer>--> +<!-- <integer name="gravity_start">8388611</integer>--> +<!-- <integer name="gravity_top">48</integer>--> +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/integers_overlayable.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/integers_overlayable.xml new file mode 100644 index 0000000..0123000 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/integers_overlayable.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!-- LINT.IfChange --> +<resources> + <!-- Integer definitions that can be overridden by the OEMs. + + !!! IMPORTANT !!! + Do not refer to these integers directly from views. Integers must be + referred to through theme attributes (in attrs.xml). + + Any new resource added to this file should also be added to the + overlayable.xml before OEMs can customize them. + + !!! IMPORTANT !!! + These resources must be added to the overlayable.xml file. DO NOT + add documentation here. Concentrate all documentation in public-ready + comments in the overlayable.xml file. --> + + <integer name="car_app_ui_list_max_length">6</integer> + <integer name="car_app_ui_grid_max_length">6</integer> + <integer name="car_app_ui_action_button_primary_horizontal_order">0</integer> + <integer name="car_app_ui_action_button_list_gravity">0</integer> + <integer name="car_app_ui_action_button_list_button_content_alignment">0</integer> + <integer name="car_app_ui_content_layout_gravity">@integer/gravity_center</integer> + <integer name="car_app_ui_content_gravity">@integer/gravity_center</integer> +</resources> +<!-- LINT.ThenChange(overlayable.xml) --> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/overlayable.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/overlayable.xml new file mode 100644 index 0000000..289f8bc --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/overlayable.xml @@ -0,0 +1,306 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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> + <!-- List of the resource that can be customized by the OEMs by using + Runtime Resource Overlays. + + !!! IMPORTANT !!! + + Comments on this file are used to produce automatically generated + documentation available at https://docs.partner.android.com/gas/integrate/template_host. + + Once per AAOS Host release, the following tool should be used to re-generate the publicly + documented resource list. This list constitutes an API with the OEMs. DO NOT remove or + rename an existing resource without a corresponding deprecation cycle. + + third_party/java_src/android_libs/car/aaos_host/main/com/android/car/libraries/templates/host/overlayable/tools/generateDoc.py + --> + <overlayable name="OverlayableResources"> + <policy type="system|product|vendor|signature"> + + <!-- Indicates whether OEMs have done any UI customizations. This value should be set to true + by the OEMs who wish to provide UI customization. --> + <item type="bool" name="car_app_ui_customized" /> + <!-- Indicates whether OEMs choose to ignore app provided colors on + buttons on select templates. This value should be set to true by the + OEMs who wish to ignore app provided colors on buttons on select + templates. --> + <item type="bool" name="car_app_ui_is_action_color_overridden" /> + <!-- Indicates whether buttons in the action button list (e.g. used in PaneTemplate) + stretch to fill the horizontal space. --> + <item type="bool" name="car_app_ui_action_button_list_button_stretch_horizontal" /> + + <!-- Car App Library standard color. --> + <item type="color" name="car_app_ui_standard_red" /> + <!-- Car App Library standard color. --> + <item type="color" name="car_app_ui_standard_red_dark" /> + <!-- Car App Library standard color. --> + <item type="color" name="car_app_ui_standard_green" /> + <!-- Car App Library standard color. --> + <item type="color" name="car_app_ui_standard_green_dark" /> + <!-- Car App Library standard color. --> + <item type="color" name="car_app_ui_standard_blue" /> + <!-- Car App Library standard color. --> + <item type="color" name="car_app_ui_standard_blue_dark" /> + <!-- Car App Library standard color. --> + <item type="color" name="car_app_ui_standard_yellow" /> + <!-- Car App Library standard color. --> + <item type="color" name="car_app_ui_standard_yellow_dark" /> + <!-- Default background color used on 'Action' buttons when one is not provided by the + application. --> + <item type="color" name="car_app_ui_action_button_default_background_color" /> + <!-- Background color used on 'Action' buttons marked as 'Primary', when one is not provided + by the application. --> + <item type="color" name="car_app_ui_action_button_primary_background_color" /> + <!-- Text color used on 'Action' buttons when one is not provided by the application. --> + <item type="color" name="car_app_ui_action_button_text_color" /> + <!-- Background color used on FABs (floating action buttons) when one is not provided by the + application. --> + <item type="color" name="car_app_ui_floating_button_default_background_color" /> + <!-- Text color used on FABs (floating action buttons) when one is not provided by the + application. --> + <item type="color" name="car_app_ui_floating_button_default_text_color" /> + <!-- Text color used on read-only text boxes (such as the PIN code in Sign-In template). --> + <item type="color" name="car_app_ui_read_only_text_color" /> + <!-- Background color used on read-only text boxes (such as the PIN code in Sign-In + template). --> + <item type="color" name="car_app_ui_read_only_text_background_color" /> + <!-- Edit box 'active' text color (such as the username and password in Sign-In template). --> + <item type="color" name="car_app_ui_edit_text_active_color" /> + <!-- Edit box 'enabled' text color (such as the username and password in Sign-In template). --> + <item type="color" name="car_app_ui_edit_text_enabled_color" /> + <!-- Edit box 'error' text color (such as the username and password in Sign-In template). --> + <item type="color" name="car_app_ui_edit_text_error_color" /> + <!-- Edit box 'disabled' text color (such as the username and password in Sign-In template). --> + <item type="color" name="car_app_ui_edit_text_disabled_color"/> + <!-- Text color used in 'clickable spans' (such as the ones allowed in Sign-In template). --> + <item type="color" name="car_app_ui_hyperlink_text_color" /> + <!-- The background color of a row container view to check color contrast against its contents. + This color is used only for color contrast checks, and not for actual background coloring. + Set an appropriate value if the row background color is customized. --> + <item type="color" name="car_app_ui_row_background_color" /> + <!-- The background color of a grid item view to check color contrast against its contents. + This color is used only for color contrast checks, and not for actual background coloring. + Set an appropriate value if the grid background color is customized. --> + <item type="color" name="car_app_ui_grid_item_background_color" /> + + <!-- Vertical space between an image and a text --> + <item type="dimen" name="car_app_ui_image_to_text_spacing_vertical" /> + <!-- Vertical space between a text and a control (such as an edit box to instruction text). --> + <item type="dimen" name="car_app_ui_text_to_control_spacing_vertical" /> + <!-- Vertical space between a text and a secondary control (such as an action button list view to additional text). --> + <item type="dimen" name="car_app_ui_text_to_secondary_control_spacing_vertical" /> + <!-- Vertical space between a control (such as an edit box) and a text. --> + <item type="dimen" name="car_app_ui_control_to_text_spacing_vertical" /> + <!-- Horizontal space between two controls (such two buttons in an Action Strip). --> + <item type="dimen" name="car_app_ui_control_to_control_spacing_horizontal" /> + <!-- Horizontal space around content areas such as full screen lists and grids. --> + <item type="dimen" name="car_app_ui_content_horizontal_margin" /> + <!-- Touch target size, used to define the size of header buttons, for example. --> + <item type="dimen" name="car_app_ui_touch_target_size" /> + <!-- Corner radius used across the UI except for the buttons. --> + <item type="dimen" name="car_app_ui_corner_radius" /> + <!-- Card width (expect for navigation card). If not set, the card width will be defined by + the host in proportion to the screen size. This value must be within the template host + defined range. --> + <item type="dimen" name="car_app_ui_card_width" /> + <!-- Width and height of large images (such as list and grid items, and message and + sign-in images. --> + <item type="dimen" name="car_app_ui_large_image_size" /> + <!-- Vertical space between the nav card content and its container. --> + <item type="dimen" name="car_app_ui_nav_card_padding_vertical" /> + <!-- Horizontal space between the navigation card content and its container. --> + <item type="dimen" name="car_app_ui_nav_card_padding_horizontal" /> + <!-- Horizontal space between an image and a text inside a navigation card. --> + <item type="dimen" name="car_app_ui_nav_card_image_to_text_spacing_horizontal" /> + <!-- Vertical space between an image and a text inside a navigation card. --> + <item type="dimen" name="car_app_ui_nav_card_image_to_text_spacing_vertical" /> + <!-- Size of xlarge text inside a navigation card. --> + <item type="dimen" name="car_app_ui_nav_card_xlarge_text_size" /> + <!-- Size of large text inside a navigation card. --> + <item type="dimen" name="car_app_ui_nav_card_large_text_size" /> + <!-- Vertical space applied in navigation card when lane images are present, for example. --> + <item type="dimen" name="car_app_ui_nav_card_small_padding_vertical" /> + <!-- Navigation card width. If not set, the card width will be defined by the host in + proportion to the screen size. This value must be within the host defined maximum + range. --> + <item type="dimen" name="car_app_ui_nav_card_width" /> + <!-- Size of small images inside a navigation card. --> + <item type="dimen" name="car_app_ui_nav_card_small_image_size" /> + <!-- Size of large images inside a navigation card. --> + <item type="dimen" name="car_app_ui_nav_card_large_image_size" /> + <!-- Size of an image inside a card header. --> + <item type="dimen" name="car_app_ui_card_header_image_size" /> + <!-- Horizontal space between a text (e.g. a title) and the border of a card. --> + <item type="dimen" name="car_app_ui_card_header_text_padding_horizontal" /> + <!-- Vertical space between a text (e.g. a title) and the border of a card. --> + <item type="dimen" name="car_app_ui_card_header_text_padding_vertical" /> + <!-- Horizontal space between a text (e.g. a title) and the border of a card when no header + button is included. --> + <item type="dimen" name="car_app_ui_card_header_no_button_text_margin_start" /> + <!-- Vertical space between grid items --> + <item type="dimen" name="car_app_ui_grid_item_vertical_spacing" /> + <!-- Vertical space between an image and a text inside a grid item. --> + <item type="dimen" name="car_app_ui_grid_item_image_to_text_spacing_vertical" /> + <!-- Vertical space between an two texts inside a grid item. --> + <item type="dimen" name="car_app_ui_grid_item_text_to_text_spacing_vertical" /> + <!-- Buttons height. --> + <item type="dimen" name="car_app_ui_button_height" /> + <!-- Image size inside a button. --> + <item type="dimen" name="car_app_ui_button_image_size" /> + <!-- Horizontal space between the start and end sides of a FAB or button and the action + text. The spacing is applied only when the button only has the text. + If `car_app_ui_action_button_list_button_content_alignment` is set to 1 (left) or 2 (right), this value will be ignored. --> + <item type="dimen" name="car_app_ui_button_text_horizontal_spacing" /> + <!-- Horizontal space between the icon and the text in a FAB or button. --> + <item type="dimen" name="car_app_ui_icon_button_image_to_text_spacing" /> + <!-- Horizontal space between the start side of a FAB or button and the action icon. The + spacing is applied only when the button has both icon and text. + If `car_app_ui_action_button_list_button_content_alignment` is set to 1 (left) or 2 (right), this value will be ignored. --> + <item type="dimen" name="car_app_ui_icon_button_start_spacing" /> + <!-- Horizontal space between the end side of a FAB or button and the action icon. The + spacing is applied only when the button has both icon and text. + If `car_app_ui_action_button_list_button_content_alignment` is set to 1 (left) or 2 (right), this value will be ignored. --> + <item type="dimen" name="car_app_ui_icon_button_end_spacing" /> + <!-- Corner radius applied to buttons. --> + <item type="dimen" name="car_app_ui_button_corner_radius" /> + <!-- The maximum width of a button in the action button list, e.g. used in PaneTemplate. + This value will be used only when `car_app_ui_action_button_list_button_stretch_horizontal` is set to `true`. --> + <item type="dimen" name="car_app_ui_action_button_list_button_max_width" /> + <!-- The horizontal spacing around the content in a button in the action button list, e.g. used in PaneTemplate. + This value will be used only when `car_app_ui_action_button_list_button_content_alignment` is set to 1 (left) or 2 (right). + When this value is used, `car_app_ui_icon_button_start_spacing`, `car_app_ui_icon_button_end_spacing`, and `car_app_ui_button_text_horizontal_spacing` will be ignored. --> + <item type="dimen" name="car_app_ui_button_side_alignment_spacing" /> + <!-- Edit box top vertical space --> + <item type="dimen" name="car_app_ui_edit_text_top_padding" /> + <!-- Edit box bottom vertical space --> + <item type="dimen" name="car_app_ui_edit_text_bottom_padding" /> + <!-- Edit box start side horizontal space --> + <item type="dimen" name="car_app_ui_edit_text_start_padding" /> + <!-- Edit box end side horizontal space --> + <item type="dimen" name="car_app_ui_edit_text_end_padding" /> + <!-- Vertical space between the edit box and the associated error message. --> + <item type="dimen" name="car_app_ui_edit_text_error_vertical_spacing" /> + <!-- Horizontal space between the edit box error message and its container. --> + <item type="dimen" name="car_app_ui_edit_text_error_horizontal_spacing" /> + <!-- Horizontal space around the text in read-only boxes (such as the PIN code in Sign-In + template). --> + <item type="dimen" name="car_app_ui_read_only_text_padding" /> + <!-- Width of a border around or under the edit box, showing the different states of the box. --> + <item type="dimen" name="car_app_ui_edit_text_border_width"/> + <!-- Start padding to list items in full lists (such as ListTemplate) --> + <item type="dimen" name="car_app_ui_full_row_start_padding" /> + <!-- End padding to list items in full lists (such as ListTemplate) --> + <item type="dimen" name="car_app_ui_full_row_end_padding" /> + <!-- Minimum height of a list item in half lists (such as PlaceListMapTemplate) --> + <item type="dimen" name="car_app_ui_half_row_min_height" /> + <!-- Horizontal space around list items in half lists (such as PlaceListMapTemplate) --> + <item type="dimen" name="car_app_ui_half_row_horizontal_padding" /> + <!-- Vertical space around list items in half lists (such as PlaceListMapTemplate) --> + <item type="dimen" name="car_app_ui_half_row_vertical_padding" /> + <!-- Horizontal space between image and text in half lists (such as PlaceListMapTemplate) --> + <item type="dimen" name="car_app_ui_half_row_image_to_text_spacing" /> + <!-- Horizontal space between two texts in half lists (such as PlaceListMapTemplate) --> + <item type="dimen" name="car_app_ui_half_row_text_to_text_spacing" /> + <!-- Image sizes in half lists (such as PlaceListMapTemplate) --> + <item type="dimen" name="car_app_ui_half_row_image_size" /> + <!-- Sign-in template authentication methods max width. --> + <item type="dimen" name="car_app_ui_sign_in_method_max_width" /> + + <!-- Drawable used for action buttons background. The default value will render these + actions as solid rectangles with rounded corners (corner radius defined by + 'car_app_ui_button_corner_radius'). Background color will be + 'car_app_ui_action_button_default_background_color' or + 'car_app_ui_action_button_primary_background_color', depending on whether the button + is primary or not. + Buttons have the following custom selectors: + <ul> + <li>type_primary: Indicates the button is a primary one. + <li>type_custom: Indicate the colors of this button depend on app provided colors. + </ul> + When a button is marked as 'custom', the app provided background color is applied as a + tint over this drawable. --> + <item type="drawable" name="car_app_ui_action_button_background" /> + + <!-- Maximum number of items to show in a list. This can't be lower than 6 --> + <item type="integer" name="car_app_ui_list_max_length" /> + <!-- Maximum number of items to show in a grid. This can't be lower than 6 --> + <item type="integer" name="car_app_ui_grid_max_length" /> + <!-- Indicates the horizontal order that OEMs pick for the primary action + on selected templates. + <ul> + <li>0 means no re-order + <li>1 indicates primary action should be on the left + <li>2 indicates primary action should be on the right + </ul> + On horizontal buttons, + --> + <item type="integer" name="car_app_ui_action_button_primary_horizontal_order" /> + + <!-- The gravity of action button list (e.g. used in MessageTemplate and PaneTemplate). + The possible values are: + <ul> + <li>0: center (default) + <li>1: bottom + </ul> --> + <item type="integer" name="car_app_ui_action_button_list_gravity" /> + <!-- The alignment of contents in buttons in the action button list (e.g. used in PaneTemplate). + The possible values are: + <ul> + <li>0: center (default) + <li>1: left + <li>2: right + </ul> --> + <item type="integer" name="car_app_ui_action_button_list_button_content_alignment" /> + <!-- Layout gravity for content areas (e.g content vertical alignment in Sign In Template + content).--> + <item type="integer" name="car_app_ui_content_layout_gravity"/> + <!-- Content gravity for content areas (e.g. content horizontal alignment in Sign In + Template content). --> + <item type="integer" name="car_app_ui_content_gravity"/> + + <!-- General paragraph text appareance --> + <item type="style" name="TextAppearance.CarAppUi.TextBlock" /> + <!-- Sign-in header text appareance --> + <item type="style" name="TextAppearance.CarAppUi.SignInHeader" /> + <!-- Sign-in legal notice text appareance --> + <item type="style" name="TextAppearance.CarAppUi.SignInLegal" /> + <!-- Card header appareance (e.g. Place List Template) --> + <item type="style" name="TextAppearance.CarAppUi.CardHeader" /> + <!-- Grid item title text appareance --> + <item type="style" name="TextAppearance.CarAppUi.GridItemTitle" /> + <!-- Grid item description text appareance --> + <item type="style" name="TextAppearance.CarAppUi.GridItemText" /> + <!-- Buttons text appareance --> + <item type="style" name="TextAppearance.CarAppUi.ButtonText" /> + <!-- Read-only text appareance --> + <item type="style" name="TextAppearance.CarAppUi.ReadOnlyText"/> + <!-- Style applied to input views (e.g. Sign-In username box) --> + <item type="style" name="Widget.CarAppUi.InputView" /> + <!-- Style applied to edit boxes --> + <item type="style" name="Widget.CarAppUi.EditText" /> + <!-- Style applied to row sections headers (such as in ListTemplate) --> + <item type="style" name="Widget.CarAppUi.RowSectionHeader" /> + <!-- Style applied to row title (such as in ListTemplate) --> + <item type="style" name="Widget.CarAppUi.RowTitle" /> + <!-- Style applied to row secondary text (such as in ListTemplate) --> + <item type="style" name="Widget.CarAppUi.RowSecondary" /> + <!-- Style applied to list empty text (such as in ListTemplate) --> + <item type="style" name="Widget.CarAppUi.RowListEmpty" /> + </policy> + </overlayable> +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/styles_overlayable.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/styles_overlayable.xml new file mode 100644 index 0000000..2162364 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/styles_overlayable.xml @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!-- LINT.IfChange --> +<resources> + <!-- Style definitions that can be overridden by the OEMs. + + !!! IMPORTANT !!! + Do not refer to these styles directly from views. Styles must be referred + to through theme attributes (in attrs.xml). + + Any new resource added to this file should also be added to the + overlayable.xml before OEMs can customize them. --> + + <!-- Template textAppearance --> + <style name="TextAppearance.CarAppUi.TextBlock" parent="TextAppearance.CarUi.Body2" /> + <style name="TextAppearance.CarAppUi.SignInHeader" parent="TextAppearance.CarUi.Body2" /> + <style name="TextAppearance.CarAppUi.SignInLegal" parent="TextAppearance.CarUi.Body3" /> + <style name="TextAppearance.CarAppUi.CardHeader" parent="TextAppearance.CarUi.Body2" /> + <style name="TextAppearance.CarAppUi.GridItemTitle" parent="TextAppearance.CarUi.Body2" /> + <style name="TextAppearance.CarAppUi.GridItemText" parent="TextAppearance.CarUi.Body3"> + <item name="android:textColor">@color/car_ui_text_color_secondary</item> + </style> + <style name="TextAppearance.CarAppUi.ButtonText" parent="TextAppearance.CarUi.Body3" /> + <style name="TextAppearance.CarAppUi.ReadOnlyText" parent="TextAppearance.CarUi.Body3"> + <item name="android:textColor">@color/car_app_ui_read_only_text_color</item> + </style> + + <!-- Input view styling --> + <style name="Widget.CarAppUi.InputView" parent=""> + <item name="android:gravity">start</item> + </style> + + <!-- Edit text styling --> + <style name="Widget.CarAppUi.EditText" parent="android:Widget.DeviceDefault.EditText"> + <item name="android:textColor">@color/default_edit_text_color_selector</item> + <item name="android:textColorHint">@color/default_edit_text_hint_color_selector</item> + <item name="android:paddingTop">@dimen/car_app_ui_edit_text_top_padding</item> + <item name="android:paddingBottom">@dimen/car_app_ui_edit_text_bottom_padding</item> + <item name="android:paddingStart">@dimen/car_app_ui_edit_text_start_padding</item> + <item name="android:paddingEnd">@dimen/car_app_ui_edit_text_end_padding</item> + <item name="android:background">@drawable/default_edit_text_background</item> + <item name="android:foreground">@drawable/default_edit_text_foreground</item> + </style> + + <!-- The style of the list section header. --> + <style name="Widget.CarAppUi.RowSectionHeader" parent="TextAppearance.CarUi.ListItem.Header"> + <item name="android:textAlignment">textStart</item> + <item name="android:layout_marginStart">@dimen/car_ui_padding_4</item> + <item name="android:layout_marginVertical">@dimen/car_ui_padding_2</item> + </style> + + <!-- The style of the title text in a list row. --> + <style name="Widget.CarAppUi.RowTitle" parent=""> + <item name="android:textAppearance">@style/TextAppearance.CarUi.ListItem</item> + <item name="android:textAlignment">viewStart</item> + <item name="android:singleLine">@bool/car_ui_list_item_single_line_title</item> + </style> + + <!-- The style of the secondary text in a list row. --> + <style name="Widget.CarAppUi.RowSecondary" parent=""> + <item name="android:textAppearance">@style/TextAppearance.CarUi.ListItem.Body</item> + <item name="android:textAlignment">viewStart</item> + </style> + + <!-- The style of text that indicates a list is empty --> + <style name="Widget.CarAppUi.RowListEmpty" parent="Widget.CarAppUi.RowSecondary"> + <item name="android:maxLines">2</item> + <item name="android:gravity">center</item> + </style> + +</resources> +<!-- LINT.ThenChange(overlayable.xml) --> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/LegacySurfaceController.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/LegacySurfaceController.kt new file mode 100644 index 0000000..bb10468 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/LegacySurfaceController.kt @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.renderer + +import android.app.Presentation +import android.content.Context +import android.hardware.display.DisplayManager +import android.hardware.display.VirtualDisplay +import android.os.Build +import android.util.Log +import android.view.KeyEvent +import android.view.MotionEvent +import android.view.Surface +import android.view.SurfaceControlViewHost +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.car.app.activity.renderer.surface.LegacySurfacePackage +import androidx.car.app.activity.renderer.surface.SurfaceControlCallback +import androidx.car.app.activity.renderer.surface.SurfaceWrapper +import androidx.car.app.serialization.Bundleable +import androidx.car.app.utils.ThreadUtils +import com.android.car.libraries.apphost.common.TemplateContext +import com.android.car.libraries.apphost.logging.LogTags +import com.android.car.libraries.apphost.logging.StatusReporter +import java.io.PrintWriter +import java.util.function.Consumer + +/** + * A presenter similar to [SurfaceControlViewHost] that conforms to [SurfaceController]. + * + * <p>This presenter should only be used if API version is lower than [Build.VERSION_CODES.R]. + * Otherwise, [SurfaceControlViewHostController] should be used. + */ +class LegacySurfaceController( + private val context: Context, + private val templateContext: TemplateContext, + private val errorHandler: Consumer<Throwable> +) : SurfaceController { + + private var presentation: Presentation? = null + private var virtualDisplay: VirtualDisplay? = null + set(value) { + field?.release()?.also { Log.d(LogTags.APP_HOST, "Released old Display") } + field = value + } + private var width: Int = 0 + private var height: Int = 0 + private var densityDpi: Int = 0 + private var contentView: View? = null + + /** An interface for listening to key events. */ + // TODO(b/192397819): Remove once SurfaceControlCallback supports the interface. + interface OnKeyListener { + /** Notifies the key event. */ + fun onKeyEvent(event: KeyEvent) + } + + private val surfaceControl = + object : SurfaceControlCallback, OnKeyListener { + override fun setSurfaceWrapper(surfaceWrapper: SurfaceWrapper) { + ThreadUtils.runOnMain { + // Since {@link SurfaceHolder.Callback} gives a guarantee that + // {@link SurfaceHolder.Callback#surfaceChanged} "is always called at least once, after" + // {@link SurfaceHolder.Callback#surfaceCreated}, we should only call + // {@link #updatePresentation} if the library is not adjusting insets. This will prevent + // two virtual displays from being created with back-to-back calls of + // {@link #setSurfaceWrapper} and {@link #relayout} when library is adjusting insets. + if (!libraryAdjustsInsets(templateContext.carHostConfig.appInfo?.libraryDisplayVersion)) { + Log.d( + LogTags.APP_HOST, + "SetSurfaceWrapper: " + "(${surfaceWrapper.width} x ${surfaceWrapper.height})" + ) + updatePresentation(surfaceWrapper) + } + } + } + + override fun onError(msg: String, e: Throwable) { + Log.e(LogTags.APP_HOST, msg, e) + errorHandler.accept(e) + } + + override fun onWindowFocusChanged(hasFocus: Boolean, isInTouchMode: Boolean) { + ThreadUtils.runOnMain { + if (contentView != null) { + presentation?.window?.setLocalFocus(hasFocus, isInTouchMode) + } + } + } + + override fun onTouchEvent(event: MotionEvent) { + ThreadUtils.runOnMain { presentation?.window?.injectInputEvent(event) } + } + + override fun onKeyEvent(event: KeyEvent) { + ThreadUtils.runOnMain { presentation?.window?.superDispatchKeyEvent(event) } + } + } + + private val surfacePackage = Bundleable.create(LegacySurfacePackage(surfaceControl)) + + override fun obtainSurfacePackage(): Bundleable = surfacePackage + + override fun releaseSurfacePackage(value: Bundleable) { + // Nothing to do here. LegacySurfacePackage doesn't need to be released. + } + + override fun releaseSurface() { + virtualDisplay?.surface = null + } + + // TODO(b/208313104): Remove once majority of 3p applications migrated to 1.2.0-alpha-02. + private fun libraryAdjustsInsets(libraryDisplayVersion: String?): Boolean { + if (libraryDisplayVersion == null || + libraryDisplayVersion.startsWith("1.1") || + libraryDisplayVersion == "1.2.0-alpha01" + ) { + return false + } + return true + } + + override fun relayout(surfaceWrapper: SurfaceWrapper) { + Log.i(LogTags.APP_HOST, "Relayout: " + "(${surfaceWrapper.width} x ${surfaceWrapper.height})") + + if (libraryAdjustsInsets(templateContext.carHostConfig.appInfo?.libraryDisplayVersion)) { + Log.i(LogTags.APP_HOST, "Library does adjust insets.") + // A size change in the surface view requires a change in the dimensions of the virtual + // display created on top of such surface. This can only be achieved by recreating the display + // and adjusting the presentation on top of it. For this to be efficient, insets changes + // should be managed on the host side (see InsetsListener), in order to avoid unnecessary + // display recreations. + updatePresentation(surfaceWrapper) + } else { + Log.i(LogTags.APP_HOST, "Library does not adjust insets.") + // When library does not adjust the insets, host gets relayout calls even when the keyboard is + // displayed. In this case we should not recreate the presentation since that will release the + // first responder and dismissed the keyboard. Instead we need to adjust the size of the + // containerView. + contentView?.layoutParams = + FrameLayout.LayoutParams(surfaceWrapper.width, surfaceWrapper.height) + } + } + + override fun setView(view: View, width: Int, height: Int) { + contentView = view + } + + override fun reportStatus(pw: PrintWriter, piiHandling: StatusReporter.Pii) { + pw.printf( + "- virtual display id: %s, width: %d, height: %d, density: %d dpi\n", + virtualDisplay?.display?.displayId ?: "-", + contentView?.layoutParams?.width ?: 0, + contentView?.layoutParams?.height ?: 0, + densityDpi + ) + } + + private fun updatePresentation(surfaceWrapper: SurfaceWrapper) { + if (!reuseVirtualDisplay(surfaceWrapper)) { + setNewDisplayAndPresentation(surfaceWrapper) + } + + // Attach contentView to Presentation if it's not already there + contentView?.takeIf { !it.isAttachedTo(presentation) }?.let { contentView -> + Log.i(LogTags.APP_HOST, "Attaching contentView to Presentation") + (contentView.parent as ViewGroup?)?.removeView(contentView) + presentation?.setContentView(contentView) + contentView.layoutParams = FrameLayout.LayoutParams(width, height) + contentView.invalidate() + } + } + + /** + * Attaches the new [Surface] to an existing [VirtualDisplay], if possible. + * + * @return [false] if there's no existing [VirtualDisplay], or its dimensions don't match. [true] + * if reuse was possible. + */ + private fun reuseVirtualDisplay(surfaceWrapper: SurfaceWrapper): Boolean { + if (virtualDisplay != null && + width == surfaceWrapper.width && + height == surfaceWrapper.height && + densityDpi == surfaceWrapper.densityDpi + ) { + Log.i(LogTags.APP_HOST, "Reusing existing VirtualDisplay with new Surface ($width x $height)") + virtualDisplay?.surface = surfaceWrapper.surface + return true + } + return false + } + + /** + * Creates, stores and shows a new [VirtualDisplay] and [Presentation] for the given + * [SurfaceWrapper]. + */ + private fun setNewDisplayAndPresentation(surfaceWrapper: SurfaceWrapper) { + Log.i( + LogTags.APP_HOST, + "Creating new VirtualDisplay and Presentation " + + "(${surfaceWrapper.width} x ${surfaceWrapper.height})" + ) + val displayManager = context.getSystemService(DisplayManager::class.java) + virtualDisplay = + displayManager.createVirtualDisplay( + VIRTUAL_DISPLAY_NAME, + surfaceWrapper.width, + surfaceWrapper.height, + surfaceWrapper.densityDpi, + surfaceWrapper.surface, + DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY + ) + width = surfaceWrapper.width + height = surfaceWrapper.height + densityDpi = surfaceWrapper.densityDpi + presentation = Presentation(PresentationContext(context), virtualDisplay?.display) + presentation?.show() + } + + protected fun finalize() { + virtualDisplay?.release() + virtualDisplay = null + width = 0 + height = 0 + densityDpi = 0 + } + + companion object { + const val VIRTUAL_DISPLAY_NAME = "ScreenRendererVirtualDisplay" + } +} + +private fun View.isAttachedTo(presentation: Presentation?): Boolean = + parent != null && parent == presentation?.findViewById(android.R.id.content) diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/PresentationContext.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/PresentationContext.kt new file mode 100644 index 0000000..d65e237 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/PresentationContext.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.renderer + +import android.content.Context +import android.content.ContextWrapper +import android.view.Display +import android.view.inputmethod.InputMethodManager + +/** + * The context used for the [Presentation] of [LegacySurfaceController]. + * + * This context injects its main [InputMethodManager] to its display contexts to avoid display + * mismatch which results in polluted logs. + */ +internal class PresentationContext(base: Context) : ContextWrapper(base) { + + private class PresentationDisplayContext( + base: Context, + private val inputMethodManager: InputMethodManager + ) : ContextWrapper(base) { + + override fun getSystemService(name: String): Any { + return if (INPUT_METHOD_SERVICE == name) inputMethodManager else super.getSystemService(name) + } + } + + override fun createDisplayContext(display: Display): Context { + val inputMethodManager = + baseContext.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + val context = super.createDisplayContext(display) + return PresentationDisplayContext(context, inputMethodManager) + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/ScreenRenderer.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/ScreenRenderer.kt new file mode 100644 index 0000000..8f7b4e0 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/ScreenRenderer.kt @@ -0,0 +1,442 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.renderer + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.res.Configuration +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.os.Message +import android.os.RemoteException +import android.os.SystemClock +import android.util.Log +import android.view.View +import androidx.annotation.ChecksSdkIntAtLeast +import androidx.annotation.VisibleForTesting +import androidx.car.app.CarAppService +import androidx.car.app.CarContext +import androidx.car.app.activity.renderer.ICarAppActivity +import androidx.car.app.activity.renderer.surface.ISurfaceListener +import androidx.car.app.activity.renderer.surface.SurfaceWrapper +import androidx.car.app.model.TemplateWrapper +import androidx.car.app.serialization.Bundleable +import androidx.car.app.utils.ThreadUtils +import com.android.car.libraries.apphost.CarHost +import com.android.car.libraries.apphost.common.BackPressedHandler +import com.android.car.libraries.apphost.common.CarAppManager +import com.android.car.libraries.apphost.common.EventManager +import com.android.car.libraries.apphost.common.HostResourceIds +import com.android.car.libraries.apphost.common.IntentUtils +import com.android.car.libraries.apphost.common.LocationMediator +import com.android.car.libraries.apphost.common.StatusBarManager +import com.android.car.libraries.apphost.common.SurfaceCallbackHandler +import com.android.car.libraries.apphost.common.TemplateContext +import com.android.car.libraries.apphost.internal.LocationMediatorImpl +import com.android.car.libraries.apphost.logging.L +import com.android.car.libraries.apphost.logging.LogTags +import com.android.car.libraries.apphost.logging.StatusReporter +import com.android.car.libraries.apphost.logging.TelemetryHandler +import com.android.car.libraries.apphost.nav.NavigationHost +import com.android.car.libraries.apphost.template.AppHost +import com.android.car.libraries.apphost.template.ConstraintHost +import com.android.car.libraries.apphost.template.UIController +import com.android.car.libraries.apphost.view.SurfaceProvider +import com.android.car.libraries.templates.host.di.FeaturesConfig +import com.android.car.libraries.templates.host.di.HostApiLevelConfig +import com.android.car.libraries.templates.host.di.ThemeManager +import com.android.car.libraries.templates.host.di.UxreConfig +import com.android.car.libraries.templates.host.internal.CarActivityDispatcher +import com.android.car.libraries.templates.host.internal.CarAppServiceInfo +import com.android.car.libraries.templates.host.internal.CarHostRepository +import com.android.car.libraries.templates.host.internal.DebugOverlayHandlerImpl +import com.android.car.libraries.templates.host.internal.InputConfigImpl +import com.android.car.libraries.templates.host.internal.InputManagerImpl +import com.android.car.libraries.templates.host.internal.InsetsListener +import com.android.car.libraries.templates.host.internal.NavigationStateCallbackImpl +import com.android.car.libraries.templates.host.internal.RendererCallback +import com.android.car.libraries.templates.host.internal.StartCarAppUtil +import com.android.car.libraries.templates.host.internal.TemplateContextImpl +import com.android.car.libraries.templates.host.view.TemplateView +import java.io.PrintWriter + +/** + * A class used to handle rendering of a single car app screen. + * + * <p>Once the activity is ready the [onCreateActivity] should be called to start the rendering. + * + * @property appName Points to the car app service which provides the data for the screen. + * @param display The display on which the content should be displayed. + */ +class ScreenRenderer( + private val context: Context, + private val appName: ComponentName, + displayId: Int, + private val callback: CarActivityDispatcher.Callback, + hostResourceIds: HostResourceIds, + uxreConfig: UxreConfig, + hostApiLevelConfig: HostApiLevelConfig, + themeManager: ThemeManager, + telemetryHandler: TelemetryHandler, + featuresConfig: FeaturesConfig, + isDebugOverlayActive: Boolean +) : BackPressedHandler, SurfaceCallbackHandler, StatusBarManager { + private var surfaceController: SurfaceController? = null + private lateinit var carActivity: CarActivityDispatcher + private val carAppManager = CarAppManagerImpl() + private val carAppServiceInfo = CarAppServiceInfo(context, appName) + private val isNavigationApp = carAppServiceInfo.isNavigationService + @VisibleForTesting var lastTemplate: TemplateWrapper? = null + private val mainHandler = Handler(Looper.getMainLooper(), HandlerCallback()) + private val inputManagerListener = + object : InputManagerImpl.InputManagerListener { + override fun onStartInput() { + carActivity.dispatch(ICarAppActivity::onStartInput) + } + + override fun onStopInput() { + carActivity.dispatch(ICarAppActivity::onStopInput) + } + + override fun onUpdateSelection( + oldSelStart: Int, + oldSelEnd: Int, + newSelStart: Int, + newSelEnd: Int + ) { + carActivity.dispatchNoFail { + it.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd) + } + } + } + + private val inputManager = InputManagerImpl(inputManagerListener) + + private val inputConfig = InputConfigImpl() + + val templateContext: TemplateContext = + TemplateContextImpl.create( + context, + appName, + displayId, + this, + this, + this, + DebugOverlayHandlerImpl(isDebugOverlayActive), + inputManager, + inputConfig, + carAppManager, + isNavigationApp, + hostResourceIds, + uxreConfig, + hostApiLevelConfig, + themeManager, + telemetryHandler, + featuresConfig + ) + + private var templateView = TemplateView.create(templateContext) + + @VisibleForTesting + val uiController = + object : UIController { + override fun getSurfaceProvider(appName: ComponentName?): SurfaceProvider { + return templateView.surfaceProvider + } + + override fun setTemplate(appName: ComponentName?, template: TemplateWrapper?) { + mainHandler.removeMessages(MSG_SET_TEMPLATE) + val msg = mainHandler.obtainMessage(MSG_SET_TEMPLATE) + msg.obj = template + + mainHandler.sendMessage(msg) + } + } + + init { + val locationMediator = + LocationMediatorImpl.create(templateContext.eventManager) { enable: Boolean -> + trySetEnableAppLocationUpdates(enable) + } + templateContext.registerAppHostService(LocationMediator::class.java, locationMediator) + } + + override fun onBackPressed() { + val carHost = CarHostRepository.get(appName) + val appHost = carHost?.getHostOrThrow(CarContext.APP_SERVICE) as? AppHost + appHost?.onBackPressed() + } + + override fun onScroll(distanceX: Float, distanceY: Float) { + val carHost = CarHostRepository.get(appName) + val appHost = carHost?.getHostOrThrow(CarContext.APP_SERVICE) as? AppHost + appHost?.onSurfaceScroll(distanceX, distanceY) + } + + override fun onFling(velocityX: Float, velocityY: Float) { + val carHost = CarHostRepository.get(appName) + val appHost = carHost?.getHostOrThrow(CarContext.APP_SERVICE) as? AppHost + appHost?.onSurfaceFling(velocityX, velocityY) + } + + override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) { + val carHost = CarHostRepository.get(appName) + val appHost = carHost?.getHostOrThrow(CarContext.APP_SERVICE) as? AppHost + appHost?.onSurfaceScale(focusX, focusY, scaleFactor) + } + + override fun setStatusBarState( + statusBarState: StatusBarManager.StatusBarState?, + rootView: View? + ) { + // TODO: Not yet implemented + Log.v( + LogTags.APP_HOST, + "StatusBar state updated to $statusBarState. " + "RootView is $rootView." + ) + } + + /** Requests to enable or disable location updates from the app. */ + private fun trySetEnableAppLocationUpdates(enabled: Boolean) { + val carHost = CarHostRepository.get(appName) + val appHost = carHost?.getHostOrThrow(CarContext.APP_SERVICE) as? AppHost + appHost?.trySetEnableLocationUpdates(enabled) + } + + private fun createBinderIntent(intent: Intent) = + Intent().apply { + action = CarAppService.SERVICE_INTERFACE + component = appName + IntentUtils.embedOriginalIntent(this, intent) + } + + fun onCreateActivity(carActivity: ICarAppActivity) { + this.carActivity = CarActivityDispatcher(appName, carActivity, callback) + val carHost = CarHostRepository.computeIfAbsent(appName) { CarHost.create(templateContext) } + carHost.registerHostService(CarContext.APP_SERVICE) { appBinding -> + AppHost.create(uiController, appBinding, templateContext) + } + carHost.registerHostService(CarContext.CONSTRAINT_SERVICE) { + ConstraintHost.create(templateContext) + } + + // Register the navigation host service only if the app is a navigation app. An + // exception will be thrown if non-nav apps try to request access to the + // navigation host service. + if (templateContext.carAppPackageInfo.isNavigationApp) { + L.d(LogTags.NAVIGATION, "Registering navigation service") + carHost.registerHostService(CarContext.NAVIGATION_SERVICE) { appBinding: Any? -> + NavigationHost.create( + appBinding, + templateContext, + NavigationStateCallbackImpl.create(templateContext) + ) + } + } + + // Before returning the CarHost instance, check that the AppHost service still has a + // reference to the UiController instance of this activity, and if not, update it. + // This could happen if the activity is destroyed and re-created after, while the + // CarAppService binding remains alive through those changes. + val appHost: AppHost = carHost.getHostOrThrow(CarContext.APP_SERVICE) as AppHost + if (uiController != appHost.getUIController()) { + L.d( + LogTags.APP_HOST, + "Activity has been re-created, updating UI controller and " + + "template context in the host services" + ) + appHost.setUIController(uiController) + carHost.setTemplateContext(templateContext) + } + + templateView.setParentLifecycle(carHost.lifecycle) + templateView.setTemplateContext(templateContext) + + val surfaceListener = surfaceListener(carActivity, carHost) + carActivity.setSurfaceListener(surfaceListener) + + templateContext.eventManager.subscribeEvent(this, EventManager.EventType.CONSTRAINTS) { + reloadTemplate() + } + } + + fun onNewIntent(intent: Intent) { + val binderIntent = createBinderIntent(intent) + CarHostRepository.get(appName)?.bindToApp(binderIntent) + } + + /** Updates the context with given configuration. */ + fun onConfigurationChanged(config: Configuration) { + templateContext.updateConfiguration(config) + } + + /** + * Called when the activity has disconnected from the renderer service. This instance shouldn't be + * used again after this point. + */ + fun onDestroy() { + L.d( + LogTags.APP_HOST, + "Activity disconnected from the renderer service. " + + "Destroying its associated screen renderer." + ) + } + + fun reportStatus(pw: PrintWriter, piiHandling: StatusReporter.Pii) { + surfaceController?.reportStatus(pw, piiHandling) + pw.printf("- last template: %s\n", lastTemplate) + } + + /** Shows/hides debug overlay if isVisible is {@code true}/{@code false} respectively. */ + fun showDebugOverlay(isVisible: Boolean) { + templateContext.debugOverlayHandler.isActive = isVisible + } + + private fun surfaceListener(carActivity: ICarAppActivity, carHost: CarHost): ISurfaceListener { + return object : ISurfaceListener.Stub() { + override fun onSurfaceAvailable(surfaceWrapperBundleable: Bundleable) { + val surfaceWrapper = surfaceWrapperBundleable.get() + if (surfaceWrapper !is SurfaceWrapper) { + Log.e( + LogTags.APP_HOST, + "onSurfaceAvailable event invoked with unexpected type: $surfaceWrapper" + ) + // TODO(b/181775931): Better handle error case + return + } + + val width = surfaceWrapper.width + val height = surfaceWrapper.height + ThreadUtils.runOnMain { + val surfaceController = getOrCreateSurfaceController(surfaceWrapper) + surfaceController.setView(templateView, width, height) + this@ScreenRenderer.surfaceController = surfaceController + val surfacePackage = surfaceController.obtainSurfacePackage() + val rendererCallback = RendererCallback(carHost, inputManager) + val insetsListener = InsetsListener(templateView) + + try { + carActivity.setInsetsListener(insetsListener) + carActivity.setSurfacePackage(surfacePackage) + carActivity.registerRendererCallback(rendererCallback) + } catch (e: RemoteException) { + Log.e(LogTags.APP_HOST, "Binder invocation failed", e) + // TODO(b/181775931): Better handle error case + } + + surfaceController.releaseSurfacePackage(surfacePackage) + } + } + + override fun onSurfaceChanged(surfaceWrapperBundleable: Bundleable) { + val surfaceWrapper = surfaceWrapperBundleable.get() + if (surfaceWrapper !is SurfaceWrapper) { + Log.e( + LogTags.APP_HOST, + "onSurfaceChanged event invoked with unexpected type: $surfaceWrapper" + ) + return + } + + ThreadUtils.runOnMain { surfaceController?.relayout(surfaceWrapper) } + } + + override fun onSurfaceDestroyed(surfaceWrapperBundleable: Bundleable) { + val surfaceWrapper = surfaceWrapperBundleable.get() + if (surfaceWrapper !is SurfaceWrapper) { + Log.e( + LogTags.APP_HOST, + "onSurfaceDestroyed event invoked with unexpected type: $surfaceWrapper" + ) + return + } + + ThreadUtils.runOnMain { surfaceController?.releaseSurface() } + } + } + } + + private fun getOrCreateSurfaceController(surfaceWrapper: SurfaceWrapper): SurfaceController { + return if (SUPPORTS_SURFACE_VIEW_HOST_WRAPPER && surfaceWrapper.hostToken != null) { + SurfaceControlViewHostController(context, surfaceWrapper) + } else { + // Reuse old instance for SDK < 30 to avoid flicker (b/187841390) + surfaceController + ?: LegacySurfaceController(context, templateContext) { e -> + Log.e(LogTags.APP_HOST, "LegacySurfaceController error", e) + carActivity.disconnect() + } + } + } + + private fun reloadTemplate() { + ThreadUtils.runOnMain { lastTemplate?.let { templateView.setTemplate(it) } } + } + + private inner class CarAppManagerImpl : CarAppManager { + override fun startCarApp(intent: Intent) { + StartCarAppUtil.validateStartCarAppIntent( + context, + appName.packageName, + intent, + isNavigationApp + ) + carActivity.dispatch { it.startCarApp(intent) } + } + + override fun finishCarApp() { + carActivity.dispatch { it.finishCarApp() } + ThreadUtils.runOnMain { CarHostRepository.remove(appName) } + } + } + + companion object { + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R) + private val SUPPORTS_SURFACE_VIEW_HOST_WRAPPER = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + private const val MSG_SET_TEMPLATE = 1 + } + + /** A [Handler.Callback] used to process the message queue for the ui controller. */ + private inner class HandlerCallback : Handler.Callback { + private var lastUpdateUptimeMillis = -Long.MAX_VALUE + + override fun handleMessage(msg: Message): Boolean { + if (msg.what == MSG_SET_TEMPLATE) { + // Use SystemClock.uptimeMillis since that is what Handler uses for time. + val currentUptimeMillis: Long = SystemClock.uptimeMillis() + val updateUptimeMillis: Long = lastUpdateUptimeMillis + 1000 + if (updateUptimeMillis > currentUptimeMillis) { + val message: Message = mainHandler.obtainMessage(MSG_SET_TEMPLATE) + message.obj = msg.obj + mainHandler.removeMessages(MSG_SET_TEMPLATE) + mainHandler.sendMessageAtTime(message, updateUptimeMillis) + return true + } + lastUpdateUptimeMillis = currentUptimeMillis + val template: TemplateWrapper = msg.obj as TemplateWrapper + + lastTemplate = template + templateView.setTemplate(template) + return true + } else { + L.w(LogTags.APP_HOST, "Unknown message: %s", msg) + } + return false + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/ScreenRendererRepository.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/ScreenRendererRepository.kt new file mode 100644 index 0000000..5d5f559 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/ScreenRendererRepository.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.renderer + +import android.content.ComponentName +import com.android.car.libraries.apphost.logging.L +import com.android.car.libraries.apphost.logging.LogTags +import com.android.car.libraries.apphost.logging.StatusReporter +import com.android.car.libraries.templates.host.internal.StatusManager +import com.google.common.collect.ImmutableList +import java.io.PrintWriter +import java.util.concurrent.ConcurrentHashMap +import java.util.function.Supplier + +/** A cache to store instances of active [ScreenRenderer] that is safe for concurrent accesses. */ +object ScreenRendererRepository : StatusReporter { + + // TODO(b/169643103): Change to Guava LRU cache to avoid having potential memory leaks. + private val cache: ConcurrentHashMap<ComponentName, ScreenRenderer> = ConcurrentHashMap() + + init { + StatusManager.addStatusReporter(StatusManager.ReportSection.SCREEN_RENDERES, this) + } + + /** + * Returns the value for the given [key]. If the key is not found in the cache, creates a + * [ScreenRenderer] using the provided [screenRendererProvider], puts its result into the map + * under the given key and returns it. + * + * This method guarantees not to put the value into the map if the key is already there, but the + * [screenRendererProvider] may be invoked even if the key is already in the map. + */ + fun computeIfAbsent( + key: ComponentName, + screenRendererProvider: Supplier<ScreenRenderer> + ): ScreenRenderer { + return cache.getOrPut(key) { screenRendererProvider.get() } + } + + /** Returns the [ScreenRenderer] for the given [key] if available. */ + fun get(key: ComponentName): ScreenRenderer? { + return cache[key] + } + + /** Returns a copy of all the available [ScreenRenderer]s. */ + fun getAll(): ImmutableList<ScreenRenderer> { + return ImmutableList.copyOf(cache.values) + } + + /** Removes the [ScreenRenderer] associated with the given [key] if available. */ + fun remove(key: ComponentName): ScreenRenderer? { + return cache.remove(key) + } + + /** Clears the cache content. */ + fun clear() { + cache.clear() + } + + override fun reportStatus(pw: PrintWriter, piiHandling: StatusReporter.Pii) { + try { + pw.println("ScreenRenderer cache") + pw.printf("- size: %d\n", cache.size) + pw.printf("- screenRenderers: %d\n", cache.size) + for ((name, value) in cache.toSortedMap()) { + pw.println("\n-------------------------------") + pw.printf("App: %s\n", name.flattenToShortString()) + value.reportStatus(pw, piiHandling) + } + } catch (t: Throwable) { + L.e(LogTags.APP_HOST, t, "Failed to produce status report for screen renderer cache") + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/SurfaceControlViewHostController.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/SurfaceControlViewHostController.kt new file mode 100644 index 0000000..c9a47e8 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/SurfaceControlViewHostController.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.renderer + +import android.content.Context +import android.hardware.display.DisplayManager +import android.os.Build +import android.view.SurfaceControlViewHost +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.annotation.RequiresApi +import androidx.car.app.activity.renderer.surface.SurfaceWrapper +import androidx.car.app.serialization.Bundleable +import com.android.car.libraries.apphost.logging.StatusReporter +import java.io.PrintWriter +import java.lang.IllegalStateException + +/** A simple wrapper around [SurfaceControlViewHost] that conforms to [SurfaceController]. */ +@RequiresApi(Build.VERSION_CODES.R) +class SurfaceControlViewHostController(val context: Context, val surfaceWrapper: SurfaceWrapper) : + SurfaceController { + + private var surfaceControlViewHost: SurfaceControlViewHost + private var width: Int? = null + private var height: Int? = null + + init { + val displayManager = context.getSystemService(DisplayManager::class.java) + val display = displayManager.getDisplay(surfaceWrapper.displayId) + val hostToken = surfaceWrapper.hostToken + surfaceControlViewHost = SurfaceControlViewHost(context, display, hostToken) + } + + /** + * Because we are wrapping the [SurfacePackage] inside a [Bundleable], automatic releasing is not + * happening. Instead it must be released manually using [releaseSurfacePackage] once this value + * has been sent to the remote process. + * + * @see [SurfacePackage] Javadoc on recommendations around releasing this value. + */ + override fun obtainSurfacePackage(): Bundleable { + val surfacePackage = + surfaceControlViewHost.surfacePackage + ?: throw IllegalStateException( + "SurfaceControlViewHost returned a null " + "SurfacePackage, which should never happen" + ) + return Bundleable.create(surfacePackage) + } + + override fun releaseSurfacePackage(value: Bundleable) { + (value.get() as SurfaceControlViewHost.SurfacePackage).release() + } + + override fun relayout(surfaceWrapper: SurfaceWrapper) { + width = surfaceWrapper.width + height = surfaceWrapper.height + surfaceControlViewHost.relayout(surfaceWrapper.width, surfaceWrapper.height) + } + + override fun setView(view: View, width: Int, height: Int) { + this.width = width + this.height = height + + // SurfaceControlViewHost doesn't provide a way to detach the view hierarchy once attached. + // We add an intermediate ViewGroup here so we can detach TemplateView and reuse it in a + // different surface if needed. + val contentView = FrameLayout(context) + contentView.layoutParams = + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + (view.parent as ViewGroup?)?.removeView(view) + contentView.addView( + view, + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + ) + + surfaceControlViewHost.setView(contentView, width, height) + } + + override fun releaseSurface() { + // No-op. Releasing surface is handled by the surface package. + } + + override fun reportStatus(pw: PrintWriter, piiHandling: StatusReporter.Pii) { + pw.printf("- display id: %d, width: %d, height: %d\n", surfaceWrapper.displayId, width, height) + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/SurfaceController.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/SurfaceController.kt new file mode 100644 index 0000000..d831f84 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/SurfaceController.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.renderer + +import android.view.View +import androidx.car.app.activity.renderer.surface.SurfaceWrapper +import androidx.car.app.serialization.Bundleable +import com.android.car.libraries.apphost.logging.StatusReporter + +/** An interface used for presenters who want to present a surface control host. */ +interface SurfaceController : StatusReporter { + /** + * Returns a surface package object in form of a [Bundleable]. This surface package must be + * released calling [releaseSurfacePackage] + */ + fun obtainSurfacePackage(): Bundleable + + /** Releases a surface package previously obtained with [obtainSurfacePackage] */ + fun releaseSurfacePackage(value: Bundleable) + + /** Relayout the surface using the given [width] and [height]. */ + fun relayout(surfaceWrapper: SurfaceWrapper) + + /** Updates the top level content view with given [view]. */ + fun setView(view: View, width: Int, height: Int) + + /** Releases the surface. Should be called once the surface is destroyed. */ + fun releaseSurface() +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/testing/TestHostApiLevelConfig.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/testing/TestHostApiLevelConfig.java new file mode 100644 index 0000000..34b4203 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/testing/TestHostApiLevelConfig.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.testing; + +import android.content.ComponentName; +import com.android.car.libraries.templates.host.di.HostApiLevelConfig; + +/** A test implementation of {@link HostApiLevelConfig} */ +public class TestHostApiLevelConfig implements HostApiLevelConfig { + + private static final TestHostApiLevelConfig INSTANCE = new TestHostApiLevelConfig(); + + /** Returns a {@link TestHostApiLevelConfig} implementation */ + public static TestHostApiLevelConfig getInstance() { + return INSTANCE; + } + + @Override + public int getHostMinApiLevel(int defaultValue, ComponentName componentName) { + return defaultValue; + } + + @Override + public int getHostMaxApiLevel(int defaultValue, ComponentName componentName) { + return defaultValue; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/testing/TestUxreConfig.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/testing/TestUxreConfig.java new file mode 100644 index 0000000..9472b75 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/testing/TestUxreConfig.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.testing; + +import com.android.car.libraries.templates.host.di.UxreConfig; + +/** A test implementation of {@link UxreConfig} */ +public class TestUxreConfig implements UxreConfig { + + private static final TestUxreConfig INSTANCE = new TestUxreConfig(); + + /** Returns a {@link TestUxreConfig} implementation */ + public static TestUxreConfig getInstance() { + return INSTANCE; + } + + @Override + public int getTemplateStackMaxSize(int defaultValue) { + return defaultValue; + } + + @Override + public int getRouteListMaxLength(int defaultValue) { + return defaultValue; + } + + @Override + public int getPaneMaxLength(int defaultValue) { + return defaultValue; + } + + @Override + public int getGridMaxLength(int defaultValue) { + return defaultValue; + } + + @Override + public int getListMaxLength(int defaultValue) { + return defaultValue; + } + + @Override + public int getCarAppDefaultMaxStringLength(int defaultValue) { + return defaultValue; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/TemplateView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/TemplateView.java new file mode 100644 index 0000000..2d76c90 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/TemplateView.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view; + +import static java.lang.Math.max; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Insets; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowInsets; +import android.widget.FrameLayout; +import android.widget.TextView; +import androidx.car.app.model.Template; +import androidx.car.app.model.TemplateWrapper; +import com.android.car.libraries.apphost.common.DebugOverlayHandler.Observer; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.view.AbstractTemplateView; +import com.android.car.libraries.apphost.view.SurfaceProvider; +import com.android.car.libraries.apphost.view.SurfaceViewContainer; +import com.android.car.libraries.apphost.view.TemplateTransitionManager; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.view.common.TemplateTransitionManagerImpl; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A view that displays {@link Template}s. + * + * <p>The current template can be set with {@link #setTemplate} method. + */ +public class TemplateView extends AbstractTemplateView implements Observer { + /** + * The {@link SurfaceViewContainer} which holds the surface that 3p apps can use to render custom + * content. + */ + private SurfaceViewContainer mSurfaceViewContainer; + + /** The {@link FrameLayout} container which holds the currently set template. */ + private FrameLayout mTemplateContainer; + + /** The {@link TextView} container which holds debug overlay info. */ + private TextView mDebugOverlayText; + + /** See {@link AbstractTemplateView#getMinimumTopPadding()} */ + private final int mMinimumTopPadding; + + /** {@link TemplateTransitionManager} used by this {@link AbstractTemplateView} implementation */ + private final TemplateTransitionManager mTransitionManager = new TemplateTransitionManagerImpl(); + + /** Creates a new instance of {@link TemplateView}. */ + @SuppressLint("InflateParams") + public static TemplateView create(TemplateContext context) { + TemplateView templateView = + (TemplateView) LayoutInflater.from(context).inflate(R.layout.template_view, null); + context.getDebugOverlayHandler().setObserver(templateView); + return templateView; + } + + public TemplateView(Context context) { + this(context, null); + } + + public TemplateView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public TemplateView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + final int[] themeAttrs = {R.attr.templateStatusBarMinimumTopPadding}; + TypedArray ta = context.obtainStyledAttributes(themeAttrs); + mMinimumTopPadding = ta.getDimensionPixelSize(0, 0); + ta.recycle(); + } + + /** + * Returns a {@link SurfaceProvider} which can be used to retrieve the {@link + * android.view.Surface} that 3p apps can use to draw custom content. + */ + @Override + public SurfaceProvider getSurfaceProvider() { + return mSurfaceViewContainer; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mSurfaceViewContainer = findViewById(R.id.surface_container); + mTemplateContainer = findViewById(R.id.template_container); + mDebugOverlayText = findViewById(R.id.debug_overlay); + } + + @Override + protected SurfaceViewContainer getSurfaceViewContainer() { + return mSurfaceViewContainer; + } + + @Override + protected ViewGroup getTemplateContainer() { + return mTemplateContainer; + } + + @Override + protected int getMinimumTopPadding() { + return mMinimumTopPadding; + } + + @Override + protected TemplateTransitionManager getTransitionManager() { + return mTransitionManager; + } + + @Override + public void setWindowInsets(WindowInsets windowInsets) { + super.setWindowInsets(windowInsets); + + if (mDebugOverlayText == null) { + return; + } + + int leftInset; + int topInset; + int rightInset; + int bottomInset; + + if (VERSION.SDK_INT >= VERSION_CODES.R) { + Insets insets = + windowInsets.getInsets(WindowInsets.Type.systemBars() | WindowInsets.Type.ime()); + leftInset = insets.left; + topInset = insets.top; + rightInset = insets.right; + bottomInset = insets.bottom; + + } else { + leftInset = windowInsets.getSystemWindowInsetLeft(); + topInset = windowInsets.getSystemWindowInsetTop(); + rightInset = windowInsets.getSystemWindowInsetRight(); + bottomInset = windowInsets.getSystemWindowInsetBottom(); + } + + FrameLayout.LayoutParams lp = (LayoutParams) mDebugOverlayText.getLayoutParams(); + lp.setMargins(leftInset, max(topInset, getMinimumTopPadding()), rightInset, bottomInset); + } + + @Override + public void setTemplate(TemplateWrapper templateWrapper) { + super.setTemplate(templateWrapper); + + TemplateContext templateContext = getTemplateContext(); + if (templateContext != null) { + templateContext.getDebugOverlayHandler().resetTemplateDebugOverlay(templateWrapper); + } + } + + @Override + public void entriesUpdated() { + TemplateContext templateContext = getTemplateContext(); + if (templateContext != null) { + setDebugOverlayText(templateContext.getDebugOverlayHandler().getDebugOverlayText()); + setDebugOverlayVisibility(templateContext.getDebugOverlayHandler().isActive()); + } + } + + private void setDebugOverlayText(CharSequence text) { + if (mDebugOverlayText == null) { + return; + } + + mDebugOverlayText.setText(text); + } + + private void setDebugOverlayVisibility(boolean isVisible) { + if (mDebugOverlayText == null) { + return; + } + + mDebugOverlayText.setVisibility(isVisible ? View.VISIBLE : View.GONE); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/animation/AnimationListenerAdapter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/animation/AnimationListenerAdapter.java new file mode 100644 index 0000000..6034f7b --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/animation/AnimationListenerAdapter.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.animation; + +import android.view.animation.Animation; + +/** + * Provides empty implementations of the methods in {@link Animation.AnimationListener} for + * convenience reasons. + */ +public class AnimationListenerAdapter implements Animation.AnimationListener { + @Override + public void onAnimationStart(Animation animation) {} + + @Override + public void onAnimationEnd(Animation animation) {} + + @Override + public void onAnimationRepeat(Animation animation) {} +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/common/TemplateTransitionManagerImpl.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/common/TemplateTransitionManagerImpl.java new file mode 100644 index 0000000..d144770 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/common/TemplateTransitionManagerImpl.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.common; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.SuppressLint; +import android.content.res.TypedArray; +import android.transition.Scene; +import android.transition.Transition; +import android.transition.TransitionInflater; +import android.transition.TransitionManager; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.Nullable; +import androidx.annotation.StyleableRes; +import com.android.car.libraries.apphost.view.TemplatePresenter; +import com.android.car.libraries.apphost.view.TemplateTransitionManager; +import com.android.car.libraries.templates.host.R; + +/** Controls transitions between different templates. */ +public class TemplateTransitionManagerImpl implements TemplateTransitionManager { + private static final float TRANSITION_ALPHA_GONE = 0f; + private static final float TRANSITION_ALPHA_VISIBLE = 1f; + + @Override + public void transition( + ViewGroup root, View surface, TemplatePresenter to, @Nullable TemplatePresenter from) { + boolean toFullScreen = to.isFullScreen(); + boolean fromFullScreen = from == null || from.isFullScreen(); + + if (toFullScreen || fromFullScreen) { + transitionDefault(root, surface, to, from); + } else { + transitionBetweenHalfScreenTemplates(root, to); + } + } + + private static void transitionBetweenHalfScreenTemplates(ViewGroup root, TemplatePresenter to) { + Scene endingScene = new Scene(root, to.getView()); + Transition transition = + TransitionInflater.from(root.getContext()) + .inflateTransition(R.transition.half_screen_to_half_screen_transition); + + TransitionManager.go(endingScene, transition); + } + + @SuppressLint("Recycle") + private static void transitionDefault( + ViewGroup root, View surface, TemplatePresenter to, @Nullable TemplatePresenter from) { + @StyleableRes final int[] themeAttrs = {R.attr.templateUpdateAnimationDurationMilliseconds}; + TypedArray ta = root.getContext().obtainStyledAttributes(themeAttrs); + long animationDurationMillis = ta.getInteger(0, 0); + ta.recycle(); + + if (to.usesSurface()) { + surface.setVisibility(View.VISIBLE); + } + + View toView = to.getView(); + View fromView = from == null ? null : from.getView(); + + toView.setAlpha(TRANSITION_ALPHA_GONE); + root.addView(toView); + + toView.animate().alpha(TRANSITION_ALPHA_VISIBLE).setDuration(animationDurationMillis); + + if (fromView != null) { + fromView + .animate() + .alpha(TRANSITION_ALPHA_GONE) + .setDuration(animationDurationMillis) + .setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (!to.usesSurface()) { + surface.setVisibility(View.GONE); + } + root.removeView(fromView); + } + }); + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/common/res/transition/half_screen_to_half_screen_transition.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/common/res/transition/half_screen_to_half_screen_transition.xml new file mode 100644 index 0000000..ca204ef --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/common/res/transition/half_screen_to_half_screen_transition.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<transitionSet xmlns:android="http://schemas.android.com/apk/res/android" + android:transitionOrdering="together"> + <fade + android:fadingMode="fade_in_out" + android:duration="?templateUpdateAnimationDurationMilliseconds"> + <targets> + <target android:excludeId="@id/map_container" /> + </targets> + </fade> + <changeBounds + android:duration="?templateUpdateAnimationDurationMilliseconds" + android:interpolator="@interpolator/fast_out_slow_in"/> +</transitionSet> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/common/res/values/ids.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/common/res/values/ids.xml new file mode 100644 index 0000000..f957662 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/common/res/values/ids.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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> + <item name="map_container" type="id"/> + <item name="content_container" type="id"/> +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/AudioRecordThread.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/AudioRecordThread.java new file mode 100644 index 0000000..d2b3997 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/AudioRecordThread.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.presenters.common; + +import android.os.ParcelFileDescriptor; +import androidx.annotation.VisibleForTesting; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import java.io.IOException; +import java.io.InputStream; + +/** A class that is used to write bytes to an {@link OutputStream} from an [@link AudioRecord} */ +final class AudioRecordThread extends Thread { + + private static final int AUDIO_RECORD_BUFFER_SIZE_BYTES = 512; + + private final ParcelFileDescriptor.AutoCloseOutputStream mOutputStream; + private final InputStream mInputStream; + private boolean mIsRecording; + private final MicrophoneClosedListener mMicrophoneClosedListener; + + AudioRecordThread( + ParcelFileDescriptor inputDescriptor, + ParcelFileDescriptor outputDescriptor, + MicrophoneClosedListener microphoneClosedListener) { + mOutputStream = new ParcelFileDescriptor.AutoCloseOutputStream(outputDescriptor); + mInputStream = new ParcelFileDescriptor.AutoCloseInputStream(inputDescriptor); + mMicrophoneClosedListener = microphoneClosedListener; + } + + @Override + public void run() { + mIsRecording = true; + L.i(LogTags.TEMPLATE, "Recording START"); + while (mIsRecording) { + + // TODO(b/159207187): Consider using read blocking + byte[] bData = new byte[AUDIO_RECORD_BUFFER_SIZE_BYTES]; + try { + mInputStream.read(bData); + } catch (IOException e) { + L.w(LogTags.TEMPLATE, e, "Recording STOPPED"); + break; + } + if (bData == null) { + L.w(LogTags.TEMPLATE, "Recording STOPPED"); + break; + } + // The task may have been cancelled: + if (isInterrupted()) { + L.d(LogTags.TEMPLATE, "Recording CANCELLED"); + break; + } + + if (bData != null) { + try { + mOutputStream.write(bData, 0, AUDIO_RECORD_BUFFER_SIZE_BYTES); + } catch (IOException e) { + // If we are unable to write bytes to the outputstream + // we close the outputstream and finish recording + L.i(LogTags.TEMPLATE, "Recording DONE"); + break; + } + } + } + + L.d(LogTags.TEMPLATE, "Recording CLEANUP"); + + // TODO(b/159208600): rewrite AudioRecordThread to use a monitor instead of errors to + // communicate + closeRecordingResourcesSafe(); + } + + /** Closes all resources associated with an ongoing recording. */ + public void closeRecordingResourcesSafe() { + if (!mIsRecording) { + return; + } + try { + mOutputStream.close(); + } catch (IOException e) { + L.e(LogTags.TEMPLATE, e, "IOException closing outputstream"); + } finally { + mIsRecording = false; + if (mMicrophoneClosedListener != null) { + + mMicrophoneClosedListener.onMicrophoneClosed(); + } + } + } + + @VisibleForTesting + public MicrophoneClosedListener getMicrophoneClosedListener() { + return mMicrophoneClosedListener; + } + + @VisibleForTesting + public void setRecording(boolean isRecording) { + mIsRecording = isRecording; + } + + public boolean isRecording() { + return mIsRecording; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/CommonTemplateConverter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/CommonTemplateConverter.java new file mode 100644 index 0000000..bc91551 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/CommonTemplateConverter.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.presenters.common; + +import android.content.Context; +import androidx.car.app.model.ListTemplate; +import androidx.car.app.model.PaneTemplate; +import androidx.car.app.model.Template; +import androidx.car.app.model.TemplateWrapper; +import com.android.car.libraries.apphost.template.view.model.RowListWrapperTemplate; +import com.android.car.libraries.apphost.view.TemplateConverter; +import com.google.common.collect.ImmutableSet; +import java.util.Collection; + +/** A {@link TemplateConverter} for common templates. */ +public class CommonTemplateConverter implements TemplateConverter { + private static final CommonTemplateConverter INSTANCE = new CommonTemplateConverter(); + private static final ImmutableSet<Class<? extends Template>> SUPPORTED_TEMPLATES = + ImmutableSet.of(PaneTemplate.class, ListTemplate.class); + + /** Returns an instance of CommonTemplateConverter */ + public static CommonTemplateConverter get() { + return INSTANCE; + } + + @Override + public TemplateWrapper maybeConvertTemplate(Context context, TemplateWrapper templateWrapper) { + Template template = templateWrapper.getTemplate(); + if (template instanceof ListTemplate || template instanceof PaneTemplate) { + Template newTemplate = + RowListWrapperTemplate.wrap(context, template, templateWrapper.isRefresh()); + + TemplateWrapper newWrapper = TemplateWrapper.wrap(newTemplate, templateWrapper.getId()); + newWrapper.setRefresh(templateWrapper.isRefresh()); + newWrapper.setCurrentTaskStep(templateWrapper.getCurrentTaskStep()); + return newWrapper; + } + return templateWrapper; + } + + @Override + public Collection<Class<? extends Template>> getSupportedTemplates() { + return SUPPORTED_TEMPLATES; + } + + private CommonTemplateConverter() {} +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/CommonTemplatePresenterFactory.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/CommonTemplatePresenterFactory.java new file mode 100644 index 0000000..2f30d3d --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/CommonTemplatePresenterFactory.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.presenters.common; + +import androidx.annotation.Nullable; +import androidx.car.app.model.GridTemplate; +import androidx.car.app.model.LongMessageTemplate; +import androidx.car.app.model.MessageTemplate; +import androidx.car.app.model.SearchTemplate; +import androidx.car.app.model.Template; +import androidx.car.app.model.TemplateWrapper; +import androidx.car.app.model.signin.SignInTemplate; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.template.view.model.RowListWrapperTemplate; +import com.android.car.libraries.apphost.view.TemplatePresenter; +import com.android.car.libraries.apphost.view.TemplatePresenterFactory; +import com.google.common.collect.ImmutableSet; +import java.util.Collection; + +/** + * Implementation of a {@link TemplatePresenterFactory} for the production host, responsible for + * providing {@link TemplatePresenter} instances for the set of templates the host supports. + */ +public class CommonTemplatePresenterFactory implements TemplatePresenterFactory { + private static final CommonTemplatePresenterFactory INSTANCE = + new CommonTemplatePresenterFactory(); + private static final ImmutableSet<Class<? extends Template>> SUPPORTED_TEMPLATES = + ImmutableSet.of( + GridTemplate.class, + LongMessageTemplate.class, + MessageTemplate.class, + RowListWrapperTemplate.class, + SearchTemplate.class, + SignInTemplate.class); + + /** Returns an instance of CommonTemplatePresenterFactory */ + public static CommonTemplatePresenterFactory get() { + return INSTANCE; + } + + @Override + @Nullable + public TemplatePresenter createPresenter( + TemplateContext templateContext, TemplateWrapper templateWrapper) { + Class<? extends Template> clazz = templateWrapper.getTemplate().getClass(); + if (GridTemplate.class == clazz) { + return GridTemplatePresenter.create(templateContext, templateWrapper); + } else if (MessageTemplate.class == clazz) { + return MessageTemplatePresenter.create(templateContext, templateWrapper); + } else if (LongMessageTemplate.class == clazz) { + return LongMessageTemplatePresenter.create(templateContext, templateWrapper); + } else if (RowListWrapperTemplate.class == clazz) { + return RowListWrapperTemplatePresenter.create(templateContext, templateWrapper); + } else if (SearchTemplate.class == clazz) { + return SearchTemplatePresenter.create(templateContext, templateWrapper); + } else if (SignInTemplate.class == clazz) { + return SignInTemplatePresenter.create(templateContext, templateWrapper); + } else { + L.w( + LogTags.TEMPLATE, + "Don't know how to create a presenter for template: %s", + clazz.getSimpleName()); + } + return null; + } + + @Override + public Collection<Class<? extends Template>> getSupportedTemplates() { + return SUPPORTED_TEMPLATES; + } + + private CommonTemplatePresenterFactory() {} +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/GridTemplatePresenter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/GridTemplatePresenter.java new file mode 100644 index 0000000..c79feb9 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/GridTemplatePresenter.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.presenters.common; + +import static android.view.View.VISIBLE; + +import android.annotation.SuppressLint; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.car.app.model.ActionStrip; +import androidx.car.app.model.GridTemplate; +import androidx.car.app.model.TemplateWrapper; +import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints; +import com.android.car.libraries.apphost.view.AbstractTemplatePresenter; +import com.android.car.libraries.apphost.view.TemplatePresenter; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.view.widgets.common.ContentView; +import com.android.car.libraries.templates.host.view.widgets.common.GridWrapper; +import com.android.car.libraries.templates.host.view.widgets.common.HeaderView; + +/** A {@link TemplatePresenter} for {@link GridTemplate} instances. */ +public class GridTemplatePresenter extends AbstractTemplatePresenter { + private final ViewGroup mRootView; + private final HeaderView mHeaderView; + private final ContentView mContentView; + + /** Create a GridTemplatePresenter */ + public static GridTemplatePresenter create( + TemplateContext templateContext, TemplateWrapper templateWrapper) { + GridTemplatePresenter presenter = new GridTemplatePresenter(templateContext, templateWrapper); + presenter.update(); + return presenter; + } + + @Override + public View getView() { + return mRootView; + } + + @Override + public void onTemplateChanged() { + update(); + } + + @Override + protected View getDefaultFocusedView() { + if (mContentView.getVisibility() == VISIBLE) { + return mContentView; + } + return super.getDefaultFocusedView(); + } + + private void update() { + GridTemplate template = (GridTemplate) getTemplate(); + ActionStrip actionStrip = template.getActionStrip(); + GridWrapper gridWrapper; + if (template.isLoading()) { + gridWrapper = + GridWrapper.wrap(null) + .setIsLoading(true) + .setIsRefresh(getTemplateWrapper().isRefresh()) + .build(); + } else { + gridWrapper = + GridWrapper.wrap(template.getSingleList()) + .setIsRefresh(getTemplateWrapper().isRefresh()) + .build(); + } + + mHeaderView.setActionStrip(actionStrip, ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE); + mHeaderView.setContent(getTemplateContext(), template.getTitle(), template.getHeaderAction()); + mContentView.setGridContent(getTemplateContext(), gridWrapper); + } + + @SuppressLint("InflateParams") + @SuppressWarnings({"methodref.receiver.bound.invalid"}) + private GridTemplatePresenter(TemplateContext templateContext, TemplateWrapper templateWrapper) { + super(templateContext, templateWrapper, StatusBarState.LIGHT); + + mRootView = + (ViewGroup) + LayoutInflater.from(templateContext) + .inflate(R.layout.grid_wrapper_template_layout, null); + mContentView = mRootView.findViewById(R.id.grid_content_view); + View contentContainer = mRootView.findViewById(R.id.content_container); + mHeaderView = HeaderView.install(templateContext, contentContainer); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/LongMessageTemplatePresenter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/LongMessageTemplatePresenter.java new file mode 100644 index 0000000..afd208f --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/LongMessageTemplatePresenter.java @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.presenters.common; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static java.util.Objects.requireNonNull; + +import android.content.res.TypedArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StyleableRes; +import androidx.car.app.model.Action; +import androidx.car.app.model.ActionStrip; +import androidx.car.app.model.CarText; +import androidx.car.app.model.LongMessageTemplate; +import androidx.car.app.model.TemplateWrapper; +import androidx.recyclerview.widget.RecyclerView; +import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints; +import com.android.car.libraries.apphost.view.AbstractTemplatePresenter; +import com.android.car.libraries.apphost.view.common.ActionButtonListParams; +import com.android.car.libraries.apphost.view.common.CarTextUtils; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView; +import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView.Gravity; +import com.android.car.libraries.templates.host.view.widgets.common.HeaderView; +import com.android.car.libraries.templates.host.view.widgets.common.ParkedOnlyFrameLayout; +import com.android.car.ui.recyclerview.CarUiRecyclerView; +import com.android.car.ui.recyclerview.CarUiRecyclerView.OnScrollListener; +import com.android.car.ui.widget.CarUiTextView; +import java.util.List; +import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; + +/** + * An {@link AbstractTemplatePresenter} that shows a scrolling long form message and some actions. + */ +public class LongMessageTemplatePresenter extends AbstractTemplatePresenter { + // TODO(b/183643108): Use a common value for this constant + private static final int MAX_ALLOWED_ACTIONS = 2; + + private final ViewGroup mRootView; + private final HeaderView mHeaderView; + private final CarUiRecyclerView mRecyclerView; + private final ActionButtonListView mStickyActionButtonListView; + private final ActionButtonListView.Gravity mActionButtonListGravity; + private final ActionButtonListParams mActionButtonListParams; + private final String mDisabledActionButtonToastMessage; + + private final LongMessageAdapter mAdapter; + + /** Create a LongMessageTemplatePresenter */ + static LongMessageTemplatePresenter create( + TemplateContext templateContext, TemplateWrapper templateWrapper) { + LongMessageTemplatePresenter presenter = + new LongMessageTemplatePresenter(templateContext, templateWrapper); + presenter.update(); + return presenter; + } + + @Override + public void onTemplateChanged() { + update(); + } + + @Override + public View getView() { + return mRootView; + } + + /** Updates the view with current values in the {@link LongMessageTemplate}. */ + private void update() { + LongMessageTemplate template = (LongMessageTemplate) getTemplate(); + ActionStrip actionStrip = template.getActionStrip(); + + // If we have a title or a header action, show the header; hide it otherwise. + CarText title = template.getTitle(); + Action headerAction = template.getHeaderAction(); + if (!CarText.isNullOrEmpty(title) || headerAction != null) { + mHeaderView.setContent(getTemplateContext(), title, headerAction); + } else { + mHeaderView.setContent(getTemplateContext(), null, null); + } + + mHeaderView.setActionStrip(actionStrip, ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE); + + mAdapter.setMessage(template.getMessage()); + if (mActionButtonListGravity == Gravity.CENTER) { + // In the case of Gravity.CENTER, put the buttons in a row along with the rest of the content. + mAdapter.setActions(template.getActions()); + mStickyActionButtonListView.setVisibility(GONE); + } else { + // If action button list gravity is not Gravity.CENTER, put the buttons in the sticky action + // button list view so they stay on screen at all times. + mAdapter.setActions(null); + mStickyActionButtonListView.setVisibility(VISIBLE); + mStickyActionButtonListView.setActionList( + getTemplateContext(), template.getActions(), mActionButtonListParams); + } + + // If this update is not due to a refresh, scroll back to the top. Template presenters can + // be reused for templates of the same type, so a scroll reset would be needed for the case + // where an app pushes two long message templates in the same flow, for example, or if we at + // some point implement a pool of presenters. + // TODO(b/186244619): Add unit test to cover this path. + if (!getTemplateWrapper().isRefresh()) { + mRecyclerView.scrollToPosition(0); + } + + setActionButtonEnabledState(); + } + + private LongMessageTemplatePresenter( + TemplateContext templateContext, TemplateWrapper templateWrapper) { + super(templateContext, templateWrapper, StatusBarState.LIGHT); + + mRootView = + (ViewGroup) + LayoutInflater.from(templateContext) + .inflate(R.layout.long_message_template_layout, null); + + mRecyclerView = mRootView.findViewById(R.id.list_view); + + ParkedOnlyFrameLayout contentContainer = mRootView.findViewById(R.id.park_only_container); + mHeaderView = HeaderView.install(templateContext, contentContainer); + contentContainer.setTemplateContext(templateContext); + + mStickyActionButtonListView = mRootView.requireViewById(R.id.sticky_action_button_list_view); + + @StyleableRes + final int[] themeAttrs = { + R.attr.templateActionButtonListGravity, R.attr.templatePlainContentBackgroundColor + }; + + TypedArray ta = templateContext.obtainStyledAttributes(themeAttrs); + mActionButtonListGravity = ActionButtonListView.Gravity.values()[ta.getInt(0, 0)]; + @ColorInt int surroundingColor = ta.getColor(1, 0); + ta.recycle(); + + mDisabledActionButtonToastMessage = + templateContext + .getResources() + .getString( + templateContext.getHostResourceIds().getLongMessageTemplateDisabledActionText()); + + mActionButtonListParams = + ActionButtonListParams.builder() + .setMaxActions(MAX_ALLOWED_ACTIONS) + .setOemReorderingAllowed(true) + .setOemColorOverrideAllowed(true) + .setSurroundingColor(surroundingColor) + .build(); + + mAdapter = new LongMessageAdapter(); + mRecyclerView.setAdapter(mAdapter); + + mRecyclerView.addOnScrollListener( + new OnScrollListener() { + @Override + public void onScrolled(CarUiRecyclerView recyclerView, int dx, int dy) { + // no-op + } + + @Override + public void onScrollStateChanged(CarUiRecyclerView recyclerView, int newState) { + if (newState != RecyclerView.SCROLL_STATE_IDLE) { + return; + } + + setActionButtonEnabledState(); + } + }); + // {@link View#OnLayoutChangeListener} is required to disable sticky action buttons on first + // load. + mRecyclerView.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> + setActionButtonEnabledState()); + } + + @RequiresNonNull({ + "mRecyclerView", + "mStickyActionButtonListView", + "mDisabledActionButtonToastMessage" + }) + private void setActionButtonEnabledState( + @UnknownInitialization LongMessageTemplatePresenter this) { + if (!mRecyclerView.getView().isAttachedToWindow()) { + return; + } + // Only need to set active state for sticky action buttons since they stay on screen at all + // times. + if (mActionButtonListGravity == Gravity.CENTER) { + return; + } + + boolean enabled = !mRecyclerView.getView().canScrollVertically(/* direction= */ 1); + + if (enabled) { + mStickyActionButtonListView.enableActionButtons(); + } else { + mStickyActionButtonListView.disableActionButtons(mDisabledActionButtonToastMessage); + } + } + + /** Adapter used for rendering the long text and buttons in this template. */ + private class LongMessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { + + static final int ITEM_TYPE_MESSAGE = 1; + static final int ITEM_TYPE_ACTION = 2; + + private String mMessage; + @Nullable private List<Action> mActions; + + public void setMessage(CarText message) { + mMessage = CarTextUtils.toCharSequenceOrEmpty(getTemplateContext(), message).toString(); + notifyDataSetChanged(); + } + + public void setActions(@Nullable List<Action> actions) { + mActions = actions; + notifyDataSetChanged(); + } + + @Override + public int getItemViewType(int position) { + if (position == 0) { + return ITEM_TYPE_MESSAGE; + } else { + return ITEM_TYPE_ACTION; + } + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { + switch (viewType) { + case ITEM_TYPE_ACTION: + return new ActionsViewHolder( + LayoutInflater.from(getTemplateContext()) + .inflate(R.layout.long_message_action_layout, viewGroup, false)); + + case ITEM_TYPE_MESSAGE: + default: + return new MessageViewHolder( + LayoutInflater.from(getTemplateContext()) + .inflate(R.layout.long_message_layout, viewGroup, false)); + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + if (viewHolder instanceof MessageViewHolder) { + ((MessageViewHolder) viewHolder).bind(mMessage); + } else if (viewHolder instanceof ActionsViewHolder) { + ((ActionsViewHolder) viewHolder).bind(getTemplateContext(), requireNonNull(mActions)); + } + } + + @Override + public int getItemCount() { + return mActions == null ? 1 : 2; + } + + /** ViewHolder for a message list item */ + private class MessageViewHolder extends RecyclerView.ViewHolder { + + private final CarUiTextView mMessage; + + private MessageViewHolder(@NonNull View view) { + super(view); + mMessage = view.requireViewById(R.id.message_text); + } + + private void bind(String message) { + mMessage.setText(message); + } + } + + /** ViewHolder for actions list item */ + private class ActionsViewHolder extends RecyclerView.ViewHolder { + + private final ActionButtonListView mActionButtonListView; + + private ActionsViewHolder(@NonNull View view) { + super(view); + mActionButtonListView = view.requireViewById(R.id.action_button_list_view); + } + + private void bind(TemplateContext templateContext, List<Action> actions) { + if (!actions.isEmpty()) { + mActionButtonListView.setActionList(templateContext, actions, mActionButtonListParams); + mActionButtonListView.setVisibility(VISIBLE); + } else { + mActionButtonListView.setVisibility(GONE); + } + } + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/MessageTemplatePresenter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/MessageTemplatePresenter.java new file mode 100644 index 0000000..0fd0c08 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/MessageTemplatePresenter.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.presenters.common; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import androidx.annotation.ColorInt; +import androidx.annotation.StyleableRes; +import androidx.car.app.model.Action; +import androidx.car.app.model.CarIcon; +import androidx.car.app.model.CarText; +import androidx.car.app.model.MessageTemplate; +import androidx.car.app.model.TemplateWrapper; +import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints; +import com.android.car.libraries.apphost.view.AbstractTemplatePresenter; +import com.android.car.libraries.apphost.view.common.ActionButtonListParams; +import com.android.car.libraries.apphost.view.common.CarTextUtils; +import com.android.car.libraries.apphost.view.common.ImageUtils; +import com.android.car.libraries.apphost.view.common.ImageViewParams; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.internal.CommonUtils; +import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView; +import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView.Gravity; +import com.android.car.libraries.templates.host.view.widgets.common.CarUiTextUtils; +import com.android.car.libraries.templates.host.view.widgets.common.HeaderView; +import com.android.car.libraries.templates.host.view.widgets.common.ViewUtils; +import com.android.car.ui.widget.CarUiTextView; +import java.util.List; + +/** An {@link AbstractTemplatePresenter} that shows an alert message, some actions, and an icon. */ +public class MessageTemplatePresenter extends AbstractTemplatePresenter { + // TODO(b/183643108): Use a common MAX_ALLOWED_ACTIONS between AAOS and AAP + private static final int MAX_ALLOWED_ACTIONS = 2; + + private final ViewGroup mRootView; + private final HeaderView mHeaderView; + private final ViewGroup mProgressContainer; + private final CarUiTextView mMessageTextView; + private final ViewGroup mStackTraceContainer; + private final CarUiTextView mStackTraceView; + private final ImageView mIconView; + private final ActionButtonListView mActionListView; + private final ImageViewParams mImageViewParams; + private final ActionButtonListParams mActionButtonListParams; + private final boolean mIsDebugEnabled; + private final ViewGroup mMessageContainer; + + /** Create a MessageTemplatePresenter */ + public static MessageTemplatePresenter create( + TemplateContext templateContext, TemplateWrapper templateWrapper) { + MessageTemplatePresenter presenter = + new MessageTemplatePresenter(templateContext, templateWrapper); + presenter.update(); + return presenter; + } + + @Override + protected View getDefaultFocusedView() { + if (mActionListView.getVisibility() == VISIBLE) { + return mActionListView; + } + return super.getDefaultFocusedView(); + } + + @Override + public void onTemplateChanged() { + update(); + } + + @Override + public View getView() { + return mRootView; + } + + /** Updates the view with current values in the {@link MessageTemplate}. */ + private void update() { + TemplateContext templateContext = getTemplateContext(); + MessageTemplate template = (MessageTemplate) getTemplate(); + + // If we have a title or a header action, show the header; hide it otherwise. + CarText title = template.getTitle(); + Action headerAction = template.getHeaderAction(); + if (!CarText.isNullOrEmpty(title) || headerAction != null) { + mHeaderView.setContent(getTemplateContext(), title, headerAction); + } else { + mHeaderView.setContent(getTemplateContext(), null, null); + } + + mHeaderView.setActionStrip( + template.getActionStrip(), ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE); + + // Show a message if we have it, hide it otherwise. + CarText message = template.getMessage(); + if (!CarText.isNullOrEmpty(message)) { + mMessageTextView.setText( + CarUiTextUtils.fromCarText(templateContext, message, mMessageTextView.getMaxLines())); + mMessageTextView.setVisibility(VISIBLE); + + // Allow focus on the message view if there are no actions available. + mMessageTextView.setFocusable(template.getActions().isEmpty()); + } else { + mMessageTextView.setVisibility(GONE); + } + + // The icon and progress indicator are mutually exclusive, next we choose which one to + // display. + boolean isLoading = template.isLoading(); + if (isLoading) { + // If in loading state, show the progress container and hide the icon. + mProgressContainer.setVisibility(VISIBLE); + mIconView.setVisibility(GONE); + } else { + // Not in loading state: hide the progress container and show the icon, if we have one. + mProgressContainer.setVisibility(GONE); + + CarIcon icon = template.getIcon(); + boolean showIcon = icon != null; + if (showIcon) { + showIcon = ImageUtils.setImageSrc(templateContext, icon, mIconView, mImageViewParams); + } + mIconView.setVisibility(showIcon ? VISIBLE : GONE); + } + + // Show the action list if we have it, hide it otherwise. + List<Action> actionList = template.getActions(); + if (!actionList.isEmpty()) { + mActionListView.setActionList(getTemplateContext(), actionList, mActionButtonListParams); + mActionListView.setVisibility(VISIBLE); + } else { + mActionListView.setVisibility(GONE); + } + + // If we can show the debug information, add a button to the action strip that toggles it + // on and off when tapped. + CarText debugMessage = template.getDebugMessage(); + if (mIsDebugEnabled && !CarText.isNullOrEmpty(debugMessage)) { + mStackTraceView.setText(CarTextUtils.toCharSequenceOrEmpty(templateContext, debugMessage)); + mStackTraceContainer.setVisibility(VISIBLE); + addDebugToggle(templateContext); + } else { + mStackTraceContainer.setVisibility(GONE); + } + } + + private void addDebugToggle(TemplateContext templateContext) { + Drawable icon = templateContext.getDrawable(R.drawable.ic_bug_report_grey600_24dp); + mHeaderView.addToggle(icon, this::showTraceView); + } + + private void showTraceView(boolean show) { + mStackTraceContainer.setVisibility(show ? VISIBLE : GONE); + mMessageContainer.setVisibility(show ? GONE : VISIBLE); + } + + @SuppressWarnings("method.invocation.invalid") + private MessageTemplatePresenter( + TemplateContext templateContext, TemplateWrapper templateWrapper) { + super(templateContext, templateWrapper, StatusBarState.LIGHT); + + mRootView = + (ViewGroup) + LayoutInflater.from(templateContext).inflate(R.layout.message_template_layout, null); + mMessageTextView = mRootView.findViewById(R.id.message_text); + mStackTraceContainer = mRootView.findViewById(R.id.stack_trace_container); + mStackTraceView = mRootView.findViewById(R.id.stack_trace); + mProgressContainer = mRootView.findViewById(R.id.progress_container); + mIconView = mRootView.findViewById(R.id.message_icon); + ViewGroup contentContainer = mRootView.findViewById(R.id.content_container); + mHeaderView = HeaderView.install(templateContext, contentContainer); + mMessageContainer = mRootView.findViewById(R.id.message_container); + + @StyleableRes + final int[] themeAttrs = { + R.attr.templateMessageDefaultIconTint, + R.attr.templateLargeImageSizeMin, + R.attr.templateLargeImageSizeMax, + R.attr.templateActionButtonListGravity, + R.attr.templatePlainContentBackgroundColor + }; + + TypedArray ta = templateContext.obtainStyledAttributes(themeAttrs); + @ColorInt int defaultIconTint = ta.getColor(0, 0); + int largeImageSizeMin = ta.getDimensionPixelSize(1, 0); + int largeImageSizeMax = ta.getDimensionPixelSize(2, Integer.MAX_VALUE); + ActionButtonListView.Gravity actionButtonListGravity = + ActionButtonListView.Gravity.values()[ta.getInt(3, 0)]; + @ColorInt int backgroundColor = ta.getColor(4, 0); + ta.recycle(); + + mActionListView = + actionButtonListGravity == Gravity.CENTER + ? mRootView.findViewById(R.id.action_button_list_view) + : mRootView.findViewById(R.id.sticky_action_button_list_view); + + // Progress container size is OEM-customizable. Enforce the size limit here. + ViewUtils.enforceViewSizeLimit(mProgressContainer, largeImageSizeMin, largeImageSizeMax); + + mImageViewParams = + ImageViewParams.builder() + .setDefaultTint(defaultIconTint) + .setBackgroundColor(backgroundColor) + .build(); + mActionButtonListParams = + ActionButtonListParams.builder() + .setMaxActions(MAX_ALLOWED_ACTIONS) + .setOemReorderingAllowed(true) + .setOemColorOverrideAllowed(true) + .setSurroundingColor(backgroundColor) + .build(); + mIsDebugEnabled = CommonUtils.INSTANCE.isDebugEnabled(templateContext); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/MicrophoneClosedListener.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/MicrophoneClosedListener.java new file mode 100644 index 0000000..9294e3c --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/MicrophoneClosedListener.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.presenters.common; + +/** + * A listener which will be notified whenever the microphone is no longer being recorded. Will allow + * for UI to be updated from the watevra host. + */ +public interface MicrophoneClosedListener { + /** Callback for when the microphone is closed. */ + void onMicrophoneClosed(); +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/PresenterUtils.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/PresenterUtils.java new file mode 100644 index 0000000..4f07f3c --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/PresenterUtils.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.presenters.common; + +import android.content.Context; +import android.content.res.Resources; +import android.view.ViewGroup; +import android.view.ViewGroup.MarginLayoutParams; + +/** Assorted presenter utilities. */ +public abstract class PresenterUtils { + /** + * Applies the top window insets of the root view of a template to the {@code viewContainer}. + * + * <p>This is needed for templates that use a overlaid view on a background surface, so that the + * status bar is rendered above the surface, and the view container is moved down so that it is + * not drawn under the status bar text. + */ + public static void applyTopWindowInsetsToContainer(int topInset, ViewGroup viewContainer) { + ViewGroup.LayoutParams layoutParams = viewContainer.getLayoutParams(); + if (layoutParams instanceof MarginLayoutParams) { + ((MarginLayoutParams) layoutParams).topMargin = topInset; + viewContainer.setLayoutParams(layoutParams); + } + } + + /** + * Returns the margin value to be applied on the left and right side to set a view's width to be a + * fraction of the screen width. + */ + public static int getAdaptiveMargin(Context context, float containerWidthFraction) { + Resources resources = context.getResources(); + if (resources == null) { + return 0; + } + int screenWidth = resources.getDisplayMetrics().widthPixels; + + float marginFraction = (1.f - containerWidthFraction) / 2; + return (int) (screenWidth * marginFraction); + } + + private PresenterUtils() {} +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/RowListWrapperTemplatePresenter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/RowListWrapperTemplatePresenter.java new file mode 100644 index 0000000..ea049fd --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/RowListWrapperTemplatePresenter.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.presenters.common; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + +import android.annotation.SuppressLint; +import android.content.res.TypedArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.ColorInt; +import androidx.annotation.StyleableRes; +import androidx.car.app.model.Action; +import androidx.car.app.model.ActionStrip; +import androidx.car.app.model.TemplateWrapper; +import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.template.view.model.RowListWrapper; +import com.android.car.libraries.apphost.template.view.model.RowListWrapperTemplate; +import com.android.car.libraries.apphost.view.AbstractTemplatePresenter; +import com.android.car.libraries.apphost.view.TemplatePresenter; +import com.android.car.libraries.apphost.view.common.ActionButtonListParams; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView; +import com.android.car.libraries.templates.host.view.widgets.common.ContentView; +import com.android.car.libraries.templates.host.view.widgets.common.HeaderView; +import java.util.List; + +/** A {@link TemplatePresenter} for {@link RowListWrapperTemplate} instances. */ +public class RowListWrapperTemplatePresenter extends AbstractTemplatePresenter { + // TODO(b/183643108): Use a common value for this constant + private static final int MAX_ALLOWED_ACTIONS = 2; + + private final ViewGroup mRootView; + private final HeaderView mHeaderView; + private final ContentView mContentView; + private final ActionButtonListView mStickyActionButtonListView; + private final ActionButtonListParams mActionButtonListParams; + + /** Create a RowListWrapperTemplatePresenter */ + public static RowListWrapperTemplatePresenter create( + TemplateContext templateContext, TemplateWrapper templateWrapper) { + RowListWrapperTemplatePresenter presenter = + new RowListWrapperTemplatePresenter(templateContext, templateWrapper); + presenter.update(); + return presenter; + } + + @Override + public View getView() { + return mRootView; + } + + @Override + public void onTemplateChanged() { + update(); + } + + @Override + protected View getDefaultFocusedView() { + if (mContentView.getVisibility() == VISIBLE) { + return mContentView; + } + return super.getDefaultFocusedView(); + } + + private void update() { + RowListWrapperTemplate template = (RowListWrapperTemplate) getTemplate(); + ActionStrip actionStrip = template.getActionStrip(); + RowListWrapper list = template.getList(); + + List<Action> actionList = template.getActionList(); + if (actionList != null && !actionList.isEmpty()) { + mStickyActionButtonListView.setVisibility(VISIBLE); + mStickyActionButtonListView.setActionList( + getTemplateContext(), actionList, mActionButtonListParams); + } else { + mStickyActionButtonListView.setVisibility(GONE); + } + + mHeaderView.setActionStrip(actionStrip, template.getActionsConstraints()); + mHeaderView.setContent(getTemplateContext(), template.getTitle(), template.getHeaderAction()); + + mContentView.setRowListContent(getTemplateContext(), list); + } + + @SuppressLint("InflateParams") + @SuppressWarnings({"methodref.receiver.bound.invalid"}) + private RowListWrapperTemplatePresenter( + TemplateContext templateContext, TemplateWrapper templateWrapper) { + super(templateContext, templateWrapper, StatusBarState.LIGHT); + + mRootView = + (ViewGroup) + LayoutInflater.from(templateContext) + .inflate(R.layout.row_list_wrapper_template_layout, null); + mContentView = mRootView.findViewById(R.id.content_view); + View contentContainer = mRootView.findViewById(R.id.content_container); + mHeaderView = HeaderView.install(templateContext, contentContainer); + + @StyleableRes final int[] themeAttrs = {R.attr.templatePlainContentBackgroundColor}; + + TypedArray ta = templateContext.obtainStyledAttributes(themeAttrs); + @ColorInt int surroundingColor = ta.getColor(0, 0); + ta.recycle(); + + mActionButtonListParams = + ActionButtonListParams.builder() + .setMaxActions(MAX_ALLOWED_ACTIONS) + .setOemReorderingAllowed(true) + .setOemColorOverrideAllowed(true) + .setSurroundingColor(surroundingColor) + .build(); + + mStickyActionButtonListView = mRootView.requireViewById(R.id.sticky_action_button_list_view); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/SearchTemplatePresenter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/SearchTemplatePresenter.java new file mode 100644 index 0000000..fbb5f84 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/SearchTemplatePresenter.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.presenters.common; + +import static android.text.InputType.TYPE_CLASS_TEXT; +import static android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS; +import static android.view.View.VISIBLE; +import static com.android.car.libraries.apphost.template.view.model.RowListWrapper.LIST_FLAGS_RENDER_TITLE_AS_SECONDARY; + +import android.annotation.SuppressLint; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import androidx.annotation.Nullable; +import androidx.car.app.model.ActionStrip; +import androidx.car.app.model.CarText; +import androidx.car.app.model.ItemList; +import androidx.car.app.model.Row; +import androidx.car.app.model.SearchTemplate; +import androidx.car.app.model.TemplateWrapper; +import com.android.car.libraries.apphost.common.EventManager; +import com.android.car.libraries.apphost.common.EventManager.EventType; +import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints; +import com.android.car.libraries.apphost.distraction.constraints.RowListConstraints; +import com.android.car.libraries.apphost.input.CarEditable; +import com.android.car.libraries.apphost.input.CarEditableListener; +import com.android.car.libraries.apphost.input.InputManager; +import com.android.car.libraries.apphost.template.view.model.RowListWrapper; +import com.android.car.libraries.apphost.template.view.model.RowWrapper; +import com.android.car.libraries.apphost.view.AbstractTemplatePresenter; +import com.android.car.libraries.apphost.view.TemplatePresenter; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.view.widgets.common.ContentView; +import com.android.car.libraries.templates.host.view.widgets.common.SearchHeaderView; + +/** + * A {@link TemplatePresenter} presenter which controls the {@link InputManager} based on values in + * the {@link SearchTemplate} model provided via {@link #update}. + */ +public class SearchTemplatePresenter extends AbstractTemplatePresenter implements CarEditable { + + private final InputManager mInputManager; + private final ViewGroup mRootView; + private final SearchHeaderView mHeaderView; + private final ContentView mContentView; + + private String mSearchHint; + private final String mDisabledSearchHint; + + private boolean mInputWasActiveOnLastWindowFocus; + + /** Creates a SearchTemplatePresenter */ + public static SearchTemplatePresenter create( + TemplateContext templateContext, TemplateWrapper templateWrapper) { + SearchTemplatePresenter presenter = + new SearchTemplatePresenter(templateContext, templateWrapper); + presenter.update(); + + return presenter; + } + + @Override + public void onStart() { + super.onStart(); + EventManager eventManager = getTemplateContext().getEventManager(); + eventManager.subscribeEvent( + this, + EventType.WINDOW_FOCUS_CHANGED, + () -> { + if (hasWindowFocus()) { + // If the input was active the last time the window was focused, it means + // that the user just dismissed the car screen keyboard. In this case, focus + // on the search result list. + if (mInputWasActiveOnLastWindowFocus) { + mContentView.requestFocus(); + } + } + mInputWasActiveOnLastWindowFocus = mInputManager.isInputActive(); + }); + } + + @Override + public void onStop() { + // TODO(b/182232738): Reenable keyboard listener + // LocationManager locationManager = LocationManager.getInstance(); + // locationManager.removeKeyboardEnabledListener(driveStatusEventListener); + getTemplateContext().getEventManager().unsubscribeEvent(this, EventType.WINDOW_FOCUS_CHANGED); + super.onStop(); + } + + @Override + public void onPause() { + mInputManager.stopInput(); + super.onPause(); + } + + @Override + public View getView() { + return mRootView; + } + + @Override + public void onTemplateChanged() { + update(); + } + + @Override + protected View getDefaultFocusedView() { + // Hide the cursor by clearing the edit text focus if input is not active + if (!mInputManager.isInputActive()) { + mHeaderView.getSearchBar().clearFocus(); + } + if (mContentView.getVisibility() == VISIBLE) { + return mContentView; + } + + return super.getDefaultFocusedView(); + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo editorInfo) { + return mHeaderView.onCreateInputConnection(editorInfo); + } + + @Override + public void setCarEditableListener(CarEditableListener listener) {} + + @Override + public void setInputEnabled(boolean enabled) {} + + private void update() { + SearchTemplate searchTemplate = (SearchTemplate) getTemplate(); + ActionStrip actionStrip = searchTemplate.getActionStrip(); + TemplateContext templateContext = getTemplateContext(); + + // Store the hint so we can set it again when the keyboard is enabled. Use a local variable + // so only one call to getSearchHint and null checker doesn't complain. + String tempSearchHint = searchTemplate.getSearchHint(); + mSearchHint = + tempSearchHint == null + ? templateContext.getString(templateContext.getHostResourceIds().getSearchHintText()) + : tempSearchHint; + updateSearchHint(mHeaderView.getSearchBar().isEnabled()); + + mHeaderView.setActionStrip(actionStrip, ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE); + + mHeaderView.setAction(searchTemplate.getHeaderAction()); + + ItemList itemList = searchTemplate.getItemList(); + boolean isEmptyList = false; + if (itemList != null && itemList.getItems().isEmpty()) { + // If the list is empty, use the first row to display the no-items message. + itemList = getItemListWithEmptyTextRow(itemList.getNoItemsMessage()); + isEmptyList = true; + } + + RowListWrapper.Builder builder = + RowListWrapper.wrap(templateContext, itemList) + .setIsLoading(searchTemplate.isLoading()) + .setRowFlags(RowWrapper.DEFAULT_UNIFORM_LIST_ROW_FLAGS) + .setRowListConstraints(RowListConstraints.ROW_LIST_CONSTRAINTS_SIMPLE) + .setIsRefresh(getTemplateWrapper().isRefresh()); + if (isEmptyList) { + builder.setListFlags(LIST_FLAGS_RENDER_TITLE_AS_SECONDARY); + } + + mContentView.setRowListContent(templateContext, builder.build()); + } + + private void updateSearchHint(boolean searchEnabled) { + mHeaderView.setHint(searchEnabled ? mSearchHint : mDisabledSearchHint); + } + + /** + * Returns an {@link ItemList} that has the no-item message sent as the text on the first row. + * + * <p>If the input no-item message is {@code null}, a default message will be added instead. + */ + private ItemList getItemListWithEmptyTextRow(@Nullable CarText customNoItemMessage) { + // Set the title to be empty because the no-item message should be rendered as secondary + // text. + String message = + getTemplateContext() + .getString(getTemplateContext().getHostResourceIds().getTemplateListNoItemsText()); + return new ItemList.Builder() + .addItem( + new Row.Builder() + .setTitle( + customNoItemMessage == null ? message : customNoItemMessage.toCharSequence()) + .build()) + .build(); + } + + @SuppressLint("InflateParams") + @SuppressWarnings({"argument.type.incompatible", "method.invocation.invalid"}) + private SearchTemplatePresenter( + TemplateContext templateContext, TemplateWrapper templateWrapper) { + super(templateContext, templateWrapper, StatusBarState.GONE); + + mInputManager = templateContext.getInputManager(); + + mRootView = + (ViewGroup) LayoutInflater.from(templateContext).inflate(R.layout.search_layout, null); + mContentView = mRootView.findViewById(R.id.content_view); + mDisabledSearchHint = + templateContext.getString(templateContext.getHostResourceIds().getSearchHintDisabledText()); + + SearchTemplate searchTemplate = (SearchTemplate) templateWrapper.getTemplate(); + View contentContainer = mRootView.findViewById(R.id.content_container); + mHeaderView = + SearchHeaderView.install( + templateContext, + contentContainer, + mRootView, + searchTemplate.getInitialSearchText(), + searchTemplate.getSearchCallbackDelegate(), + searchTemplate.isShowKeyboardByDefault()); + int inputType = + mHeaderView.getSearchBar().getInputType() | TYPE_CLASS_TEXT | TYPE_TEXT_FLAG_NO_SUGGESTIONS; + mHeaderView.getSearchBar().setInputType(inputType); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/SignInTemplatePresenter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/SignInTemplatePresenter.java new file mode 100644 index 0000000..4a09650 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/SignInTemplatePresenter.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.presenters.common; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + +import android.content.res.TypedArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import androidx.annotation.ColorInt; +import androidx.annotation.StyleableRes; +import androidx.car.app.model.Action; +import androidx.car.app.model.CarText; +import androidx.car.app.model.TemplateWrapper; +import androidx.car.app.model.signin.InputSignInMethod; +import androidx.car.app.model.signin.PinSignInMethod; +import androidx.car.app.model.signin.ProviderSignInMethod; +import androidx.car.app.model.signin.QRCodeSignInMethod; +import androidx.car.app.model.signin.SignInTemplate; +import androidx.car.app.model.signin.SignInTemplate.SignInMethod; +import com.android.car.libraries.apphost.common.EventManager; +import com.android.car.libraries.apphost.common.EventManager.EventType; +import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints; +import com.android.car.libraries.apphost.distraction.constraints.CarColorConstraints; +import com.android.car.libraries.apphost.input.InputManager; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.view.AbstractTemplatePresenter; +import com.android.car.libraries.apphost.view.TemplatePresenter; +import com.android.car.libraries.apphost.view.common.ActionButtonListParams; +import com.android.car.libraries.apphost.view.common.CarTextParams; +import com.android.car.libraries.apphost.view.common.CarTextUtils; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView; +import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView.Gravity; +import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonView; +import com.android.car.libraries.templates.host.view.widgets.common.CarUiTextUtils; +import com.android.car.libraries.templates.host.view.widgets.common.ClickableSpanTextContainer; +import com.android.car.libraries.templates.host.view.widgets.common.HeaderView; +import com.android.car.libraries.templates.host.view.widgets.common.InputSignInView; +import com.android.car.libraries.templates.host.view.widgets.common.ParkedOnlyFrameLayout; +import com.android.car.libraries.templates.host.view.widgets.common.PinSignInView; +import com.android.car.libraries.templates.host.view.widgets.common.QRCodeSignInView; +import com.android.car.ui.widget.CarUiTextView; +import java.util.List; + +/** A {@link TemplatePresenter} for {@link SignInTemplate} instances. */ +public class SignInTemplatePresenter extends AbstractTemplatePresenter { + // TODO(b/183643108): Use a common MAX_ALLOWED_ACTIONS between AAOS and AAP + private static final int MAX_ALLOWED_ACTIONS = 2; + private static final CarTextParams ADDITIONAL_TEXT_PARAMS = + CarTextParams.builder().setAllowClickableSpans(true).build(); + + private final InputManager mInputManager; + private final ViewGroup mRootView; + private final HeaderView mHeaderView; + private final LinearLayout mSignInContainer; + private final CarUiTextView mInstructionTextView; + private final ActionButtonView mProviderSignInButton; + private final InputSignInView mInputSignInView; + private final PinSignInView mPinSignInView; + private final QRCodeSignInView mQRCodeSignInView; + private final ClickableSpanTextContainer mAdditionalTextView; + private final ActionButtonListView mActionListView; + private final ParkedOnlyFrameLayout mContentContainer; + private final ProgressBar mProgressBar; + private final String mDisabledInputHint; + private final ActionButtonListParams mActionButtonListParams; + private final CarTextParams mInstructionTextParams; + private boolean mInputWasActiveOnLastWindowFocus; + + /** Create a {@link SignInTemplatePresenter} instance. */ + static SignInTemplatePresenter create( + TemplateContext templateContext, TemplateWrapper templateWrapper) { + SignInTemplatePresenter presenter = + new SignInTemplatePresenter(templateContext, templateWrapper); + presenter.update(); + return presenter; + } + + @Override + protected View getDefaultFocusedView() { + if (mProviderSignInButton.getVisibility() == VISIBLE) { + return mProviderSignInButton; + } + if (mInputSignInView.getVisibility() == VISIBLE) { + // Hide the cursor by clearing the edit text focus if input is not active + if (!mInputManager.isInputActive()) { + mInputSignInView.clearEditTextFocus(); + } + } + if (mPinSignInView.getVisibility() == VISIBLE) { + return mPinSignInView; + } + if (mActionListView.getVisibility() == VISIBLE) { + return mActionListView; + } + + return super.getDefaultFocusedView(); + } + + @Override + public void onStart() { + super.onStart(); + EventManager eventManager = getTemplateContext().getEventManager(); + eventManager.subscribeEvent( + this, + EventType.WINDOW_FOCUS_CHANGED, + () -> { + if (hasWindowFocus()) { + // If the input was active the last time the window was focused, it means + // that the user just dismissed the car screen keyboard. In this case, focus + // on the action button list. + if (mInputWasActiveOnLastWindowFocus) { + mActionListView.requestFocus(); + } + } + mInputWasActiveOnLastWindowFocus = mInputManager.isInputActive(); + }); + } + + @Override + public void onStop() { + // TODO(b/182232738): Reenable keyboard listener + // LocationManager locationManager = LocationManager.getInstance(); + // locationManager.removeKeyboardEnabledListener(driveStatusEventListener); + getTemplateContext().getEventManager().unsubscribeEvent(this, EventType.WINDOW_FOCUS_CHANGED); + super.onStop(); + } + + @Override + public void onPause() { + mInputManager.stopInput(); + super.onPause(); + } + + @Override + public void onTemplateChanged() { + update(); + } + + @Override + public View getView() { + return mRootView; + } + + /** Updates the view with current values in the {@link SignInTemplate}. */ + private void update() { + TemplateContext templateContext = getTemplateContext(); + SignInTemplate template = (SignInTemplate) getTemplate(); + + setHeaderView(templateContext, template); + setProgressBar(template); + setInstructionText(templateContext, template); + setSignInView(templateContext, template); + setAdditionalText(templateContext, template); + setActionListButtons(templateContext, template); + } + + private void setHeaderView(TemplateContext templateContext, SignInTemplate template) { + // If we have a title or a header action, show the header; hide it otherwise. + CarText title = template.getTitle(); + Action headerAction = template.getHeaderAction(); + if (!CarText.isNullOrEmpty(title) || headerAction != null) { + mHeaderView.setContent(templateContext, title, headerAction); + } else { + mHeaderView.setContent(templateContext, null, null); + } + mHeaderView.setActionStrip( + template.getActionStrip(), ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE); + } + + private void setProgressBar(SignInTemplate template) { + if (template.isLoading()) { + mProgressBar.setVisibility(VISIBLE); + mSignInContainer.setVisibility(GONE); + } else { + mProgressBar.setVisibility(GONE); + mSignInContainer.setVisibility(VISIBLE); + } + } + + private void setInstructionText(TemplateContext templateContext, SignInTemplate template) { + CarText instructions = template.getInstructions(); + if (!CarText.isNullOrEmpty(instructions)) { + mInstructionTextView.setText( + CarUiTextUtils.fromCarText( + templateContext, + instructions, + mInstructionTextParams, + mInstructionTextView.getMaxLines())); + mInstructionTextView.setVisibility(VISIBLE); + } else { + mInstructionTextView.setVisibility(GONE); + } + } + + private void setSignInView(TemplateContext templateContext, SignInTemplate template) { + // Reset the sign-in view + mProviderSignInButton.setVisibility(GONE); + mInputSignInView.setVisibility(GONE); + mPinSignInView.setVisibility(GONE); + mQRCodeSignInView.setVisibility(GONE); + SignInMethod signInMethod = template.getSignInMethod(); + if (signInMethod instanceof ProviderSignInMethod) { + ProviderSignInMethod providerSignInMethod = (ProviderSignInMethod) signInMethod; + Action providerSignInAction = providerSignInMethod.getAction(); + // OEMs cannot overwrite provider method button in Sign in template + mProviderSignInButton.setAction( + templateContext, + providerSignInAction, + ActionButtonListParams.builder().setAllowAppColor(true).build()); + mProviderSignInButton.setVisibility(VISIBLE); + } else if (signInMethod instanceof InputSignInMethod) { + InputSignInMethod inputSignInMethod = (InputSignInMethod) signInMethod; + mInputSignInView.setSignInMethod( + templateContext, + inputSignInMethod, + mInputManager, + mDisabledInputHint, + getTemplateWrapper().isRefresh()); + mInputSignInView.setVisibility(VISIBLE); + } else if (signInMethod instanceof PinSignInMethod) { + PinSignInMethod pinSignInMethod = (PinSignInMethod) signInMethod; + mPinSignInView.setText( + CarUiTextUtils.fromCarText( + templateContext, pinSignInMethod.getPinCode(), mPinSignInView.getMaxLines())); + mPinSignInView.setVisibility(VISIBLE); + } else if (signInMethod instanceof QRCodeSignInMethod) { + QRCodeSignInMethod qrCodeSignInMethod = (QRCodeSignInMethod) signInMethod; + mQRCodeSignInView.setQRCodeSignInMethod(templateContext, qrCodeSignInMethod); + mQRCodeSignInView.setVisibility(VISIBLE); + } else { + L.w(LogTags.TEMPLATE, "Unknown sign in method: %s", signInMethod); + } + } + + private void setAdditionalText(TemplateContext templateContext, SignInTemplate template) { + CarText additionalText = template.getAdditionalText(); + + if (!CarText.isNullOrEmpty(additionalText)) { + mAdditionalTextView.setText( + CarTextUtils.toCharSequenceOrEmpty( + templateContext, additionalText, ADDITIONAL_TEXT_PARAMS)); + mAdditionalTextView.setVisibility(VISIBLE); + } else { + mAdditionalTextView.setVisibility(GONE); + } + } + + private void setActionListButtons(TemplateContext templateContext, SignInTemplate template) { + List<Action> actionList = template.getActions(); + if (!actionList.isEmpty()) { + mActionListView.setActionList(templateContext, actionList, mActionButtonListParams); + mActionListView.setVisibility(VISIBLE); + } else { + mActionListView.setVisibility(GONE); + } + } + + @SuppressWarnings("nullness:method.invocation") + private SignInTemplatePresenter( + TemplateContext templateContext, TemplateWrapper templateWrapper) { + super(templateContext, templateWrapper, StatusBarState.LIGHT); + + @StyleableRes + final int[] themeAttrs = { + R.attr.templateActionButtonListGravity, R.attr.templatePlainContentBackgroundColor + }; + + TypedArray ta = templateContext.obtainStyledAttributes(themeAttrs); + ActionButtonListView.Gravity actionButtonListGravity = + ActionButtonListView.Gravity.values()[ta.getInt(0, 0)]; + @ColorInt int backgroundColor = ta.getColor(1, 0); + ta.recycle(); + + mInputManager = templateContext.getInputManager(); + mDisabledInputHint = + templateContext.getString(templateContext.getHostResourceIds().getSearchHintDisabledText()); + mActionButtonListParams = + ActionButtonListParams.builder() + .setMaxActions(MAX_ALLOWED_ACTIONS) + .setOemReorderingAllowed(false) + .setOemColorOverrideAllowed(false) + .setSurroundingColor(backgroundColor) + .build(); + mInstructionTextParams = + CarTextParams.builder() + .setColorSpanConstraints(CarColorConstraints.STANDARD_ONLY) + .setBackgroundColor(backgroundColor) + .build(); + mRootView = + (ViewGroup) + LayoutInflater.from(templateContext).inflate(R.layout.sign_in_template_layout, null); + mSignInContainer = mRootView.findViewById(R.id.sign_in_container); + mInstructionTextView = mRootView.findViewById(R.id.instruction_text); + mProviderSignInButton = mRootView.findViewById(R.id.provider_sign_in_button); + mInputSignInView = mRootView.findViewById(R.id.input_sign_in_view); + mPinSignInView = mRootView.findViewById(R.id.pin_sign_in_view); + mQRCodeSignInView = mRootView.findViewById(R.id.qr_code_sign_in_view); + mAdditionalTextView = mRootView.findViewById(R.id.additional_text); + mContentContainer = mRootView.findViewById(R.id.park_only_container); + mHeaderView = HeaderView.install(templateContext, mContentContainer); + mContentContainer.setTemplateContext(templateContext); + mProgressBar = mRootView.findViewById(R.id.sign_in_progress_bar); + mActionListView = + actionButtonListGravity == Gravity.CENTER + ? mRootView.findViewById(R.id.action_button_list_view) + : mRootView.findViewById(R.id.sticky_action_button_list_view); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_bug_report_grey600_24dp.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_bug_report_grey600_24dp.xml new file mode 100644 index 0000000..b471d0d --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_bug_report_grey600_24dp.xml @@ -0,0 +1,10 @@ +<vector + xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:viewportHeight="24" + android:viewportWidth="24" + android:width="24dp"> + <path + android:fillColor="@color/default_gray_600" + android:pathData="M20,10L20,8h-2.81c-0.45,-0.78 -1.07,-1.46 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17c-0.03,-0.01 -0.05,-0.01 -0.08,-0.01 -0.16,-0.04 -0.32,-0.06 -0.49,-0.09l-0.17,-0.03C12.46,5.02 12.23,5 12,5c-0.49,0 -0.97,0.07 -1.42,0.18l0.02,-0.01L8.41,3 7,4.41l1.62,1.63h0.01c-0.75,0.5 -1.37,1.18 -1.82,1.96L4,8v2h2.09c-0.06,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10zM16,15c0,2.21 -1.79,4 -4,4s-4,-1.79 -4,-4v-4c0,-2.21 1.79,-4 4,-4s4,1.79 4,4v4zM10,14h4v2h-4zM10,10h4v2h-4z"/> +</vector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_mic_black_48dp.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_mic_black_48dp.xml new file mode 100644 index 0000000..e1ff4ac --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_mic_black_48dp.xml @@ -0,0 +1,4 @@ +<vector android:height="48dp" android:viewportHeight="24" + android:viewportWidth="24" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#000000" android:pathData="M12,14c1.66,0 3,-1.34 3,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17,11c0,2.76 -2.24,5 -5,5s-5,-2.24 -5,-5L5,11c0,3.53 2.61,6.43 6,6.92L11,21h2v-3.08c3.39,-0.49 6,-3.39 6,-6.92h-2z"/> +</vector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_report_problem_black_48dp.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_report_problem_black_48dp.xml new file mode 100644 index 0000000..ee5bc0e --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_report_problem_black_48dp.xml @@ -0,0 +1,4 @@ +<vector android:height="48dp" android:viewportHeight="24" + android:viewportWidth="24" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#000000" android:pathData="M12,5.99L19.53,19L4.47,19L12,5.99M12,2L1,21h22L12,2zM13,16h-2v2h2v-2zM13,10h-2v4h2v-4z"/> +</vector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_volume_up_black_48dp.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_volume_up_black_48dp.xml new file mode 100644 index 0000000..c58cae9 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_volume_up_black_48dp.xml @@ -0,0 +1,4 @@ +<vector android:height="48dp" android:viewportHeight="24" + android:viewportWidth="24" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#000000" android:pathData="M3,9v6h4l5,5L12,4L7,9L3,9zM10,8.83v6.34L7.83,13L5,13v-2h2.83L10,8.83zM16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77 0,-4.28 -2.99,-7.86 -7,-8.77z"/> +</vector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/grid_wrapper_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/grid_wrapper_template_layout.xml new file mode 100644 index 0000000..3795876 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/grid_wrapper_template_layout.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + style="?attr/templatePlainContentContainerStyle" + android:orientation="vertical"> + + <!-- A container for the contents of the screen, used as the container for + the header view. --> + <LinearLayout + android:id="@+id/content_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <include + android:id="@+id/grid_content_view" + layout="@layout/content_view" + android:layout_marginTop="@dimen/car_ui_padding_3" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1"/> + </LinearLayout> +</FrameLayout> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/long_message_action_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/long_message_action_layout.xml new file mode 100644 index 0000000..8641f81 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/long_message_action_layout.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" > + <include + android:id="@+id/action_button_list_view" + layout="@layout/action_button_list_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/car_app_ui_text_to_control_spacing_vertical" + android:layout_marginBottom="@dimen/template_padding_2" /> +</FrameLayout> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/long_message_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/long_message_layout.xml new file mode 100644 index 0000000..f087180 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/long_message_layout.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<CarUiTextView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/message_text" + style="?templateMessageLongTextStyle" + android:layout_gravity="start" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:foreground="@drawable/no_content_view_focus_ring" /> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/long_message_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/long_message_template_layout.xml new file mode 100644 index 0000000..68c5af4 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/long_message_template_layout.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + style="?attr/templatePlainContentContainerStyle" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <!-- A container for the contents of the screen, used as the container for + the header view. --> + <com.android.car.libraries.templates.host.view.widgets.common.ParkedOnlyFrameLayout + android:id="@+id/park_only_container" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <com.android.car.ui.FocusArea + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <com.android.car.ui.recyclerview.CarUiRecyclerView + android:id="@+id/list_view" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:clickable="true" + android:paddingTop="@dimen/template_padding_3" + android:clipToPadding="true" + app:layoutStyle="linear"/> + + <include + android:id="@+id/sticky_action_button_list_view" + layout="@layout/action_button_list_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="?templateStickyButtonsVerticalSpacing" + android:layout_marginBottom="?templateStickyButtonsVerticalSpacing" + android:visibility="gone" /> + </com.android.car.ui.FocusArea> + </com.android.car.libraries.templates.host.view.widgets.common.ParkedOnlyFrameLayout> +</FrameLayout> + diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/message_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/message_template_layout.xml new file mode 100644 index 0000000..caca61d --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/message_template_layout.xml @@ -0,0 +1,150 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<LinearLayout + 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" + style="?attr/templatePlainContentContainerStyle" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <!-- A container for the contents of the screen, used so we can implement + margins that adapt to different screen sizes. Used as a container for + the header view.--> + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/content_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <!-- A container for the template elements that is centered in the screen. --> + <com.android.car.ui.FocusArea + android:id="@+id/message_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:gravity="center" + android:orientation="vertical" + android:layout_marginHorizontal="?templatePlainContentHorizontalPadding" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@id/sticky_action_button_focus_area" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + tools:ignore="UselessParent"> + + <!-- A progress indicator shown in place of the icon if the message + template is in loading state--> + <FrameLayout + android:id="@+id/progress_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:focusable="true" + android:visibility="gone"> + <com.android.car.libraries.templates.host.view.widgets.common.CarProgressBar + android:id="@+id/progress" + style="?templateLoadingSpinnerStyle" + android:layout_width="?templateLargeImageSize" + android:layout_height="?templateLargeImageSize" + app:imageMinSize="?templateLargeImageSizeMin" + app:imageMaxSize="?templateLargeImageSizeMax" + android:layout_gravity="center" /> + </FrameLayout> + + <!-- An icon shown on top of the contents. --> + <com.android.car.libraries.templates.host.view.widgets.common.CarImageView + android:id="@+id/message_icon" + android:layout_width="?templateLargeImageSize" + android:layout_height="?templateLargeImageSize" + app:imageMinWidth="?templateLargeImageSizeMin" + app:imageMaxWidth="?templateLargeImageSizeMax" + app:imageMinHeight="?templateLargeImageSizeMin" + app:imageMaxHeight="?templateLargeImageSizeMax" + android:visibility="gone" + tools:ignore="ContentDescription" /> + + <!-- The title displayed below the icon. --> + <CarUiTextView + android:id="@+id/message_text" + style="?templateMessageTitleTextStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginTop="?templateMessageTitleTopSpacing" + android:foreground="@drawable/no_content_view_focus_ring" /> + + <include + android:id="@+id/action_button_list_view" + layout="@layout/action_button_list_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="?templateMessageButtonsTopSpacing" + android:visibility="gone" /> + </com.android.car.ui.FocusArea> + + <!-- Stack trace section, shown only in debug when clicking on the + action to view it. --> + <ScrollView + android:id="@+id/stack_trace_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/template_padding_4" + android:layout_marginBottom="@dimen/template_padding_1" + android:layout_marginStart="@dimen/template_width_keyline_2" + android:layout_marginEnd="@dimen/template_width_keyline_2" + android:padding="@dimen/template_padding_1" + android:focusable="false" + android:layout_gravity="top" + android:visibility="gone" + android:background="?templateDebugMessageBackgroundColor" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + tools:ignore="RtlHardcoded"> + + <HorizontalScrollView + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <CarUiTextView + android:id="@+id/stack_trace" + style="?templateMessageDebugTextStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + </HorizontalScrollView> + </ScrollView> + <com.android.car.ui.FocusArea + android:id="@+id/sticky_action_button_focus_area" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent"> + <include + android:id="@+id/sticky_action_button_list_view" + layout="@layout/action_button_list_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="?templateStickyButtonsVerticalSpacing" + android:layout_marginBottom="?templateStickyButtonsVerticalSpacing" + android:visibility="gone" /> + </com.android.car.ui.FocusArea> + </androidx.constraintlayout.widget.ConstraintLayout> +</LinearLayout> + diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/row_list_wrapper_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/row_list_wrapper_template_layout.xml new file mode 100644 index 0000000..84669a8 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/row_list_wrapper_template_layout.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + style="?attr/templatePlainContentContainerStyle" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <!-- A container for the contents of the screen, used as the container for + the header view. --> + <LinearLayout + android:id="@+id/content_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <include + android:id="@+id/content_view" + layout="@layout/content_view" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1"/> + + <com.android.car.ui.FocusArea + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + <include + android:id="@+id/sticky_action_button_list_view" + layout="@layout/action_button_list_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="?templateStickyButtonsVerticalSpacing" + android:layout_marginBottom="?templateStickyButtonsVerticalSpacing" + android:visibility="gone" /> + </com.android.car.ui.FocusArea> + </LinearLayout> +</LinearLayout> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/search_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/search_layout.xml new file mode 100644 index 0000000..3bfd482 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/search_layout.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + style="?templatePlainContentContainerStyle" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <LinearLayout + android:id="@+id/content_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + <include + android:id="@+id/content_view" + layout="@layout/content_view" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1"/> + </LinearLayout> +</LinearLayout> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/sign_in_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/sign_in_template_layout.xml new file mode 100644 index 0000000..a8c56b7 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/sign_in_template_layout.xml @@ -0,0 +1,140 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> +<LinearLayout + 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" + style="?attr/templatePlainContentContainerStyle" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <!-- A container for the contents of the screen, used so we can implement + margins that adapt to different screen sizes. Used as a container for + the header view. --> + <com.android.car.libraries.templates.host.view.widgets.common.ParkedOnlyFrameLayout + android:id="@+id/park_only_container" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <!-- This FrameLayout's visibility is modified only by + ParkedOnlyFrameLayout --> + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + <!-- The loading spinner. --> + <com.android.car.libraries.templates.host.view.widgets.common.CarProgressBar + android:id="@+id/sign_in_progress_bar" + style="?templateLoadingSpinnerStyle" + android:layout_width="?templateLargeImageSize" + android:layout_height="?templateLargeImageSize" + app:imageMinSize="?templateLargeImageSizeMin" + app:imageMaxSize="?templateLargeImageSizeMax" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:visibility="gone"/> + + <!-- A container for the template elements that is centered in the + screen. --> + <LinearLayout + android:id="@+id/sign_in_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="?templateSignInContainerStyle" + android:orientation="vertical" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@+id/sticky_action_button_list_view" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + tools:ignore="UselessParent"> + + <!-- The instruction text. --> + <CarUiTextView + android:id="@+id/instruction_text" + style="?templateSignInInstructionTextStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:focusable="true" + android:layout_marginTop="@dimen/template_padding_3" + android:foreground="@drawable/no_content_view_focus_ring" /> + + <!-- Provider Sign In Method --> + <include + android:id="@+id/provider_sign_in_button" + layout="@layout/sign_in_button_view" + android:layout_width="wrap_content" + android:layout_height="?templateActionButtonHeight" + android:layout_marginTop="@dimen/car_app_ui_text_to_control_spacing_vertical" + android:visibility="gone" /> + + <!-- Input Sign In Method --> + <include + android:id="@+id/input_sign_in_view" + layout="@layout/input_sign_in_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/car_app_ui_text_to_control_spacing_vertical" + android:visibility="gone" /> + + <!-- PIN Sign In Method --> + <include + android:id="@+id/pin_sign_in_view" + layout="@layout/pin_sign_in_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/car_app_ui_text_to_control_spacing_vertical" + android:visibility="gone" /> + + <!-- QR Code Sign In Method --> + <include + android:id="@+id/qr_code_sign_in_view" + layout="@layout/qr_code_sign_in_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/car_app_ui_text_to_control_spacing_vertical" + android:visibility="gone" /> + + <include + android:id="@+id/additional_text" + layout="@layout/clickable_span_text_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/car_app_ui_control_to_text_spacing_vertical" /> + + <include + android:id="@+id/action_button_list_view" + layout="@layout/action_button_list_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/car_app_ui_text_to_secondary_control_spacing_vertical" + android:visibility="gone" /> + </LinearLayout> + <include + android:id="@+id/sticky_action_button_list_view" + layout="@layout/action_button_list_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="?templateStickyButtonsVerticalSpacing" + android:layout_marginBottom="?templateStickyButtonsVerticalSpacing" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:visibility="gone" /> + </androidx.constraintlayout.widget.ConstraintLayout> + </com.android.car.libraries.templates.host.view.widgets.common.ParkedOnlyFrameLayout> +</LinearLayout> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/voice_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/voice_layout.xml new file mode 100644 index 0000000..11b7e8e --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/voice_layout.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<LinearLayout + 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" + style="?attr/templatePlainContentContainerStyle" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <include + layout="@layout/card_header_layout" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + +<!-- A container for the contents of the screen, used so we can implement + margins that adapt to different screen sizes. --> + <FrameLayout + android:id="@+id/content_container" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <!-- A container for the template elements that is centered in the screen. We ignore + UseCompoundDrawables since are image needs an id to change with the VoiceTemplate state --> + <LinearLayout + android:id="@+id/button_with_description" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:gravity="center" + android:orientation="vertical" + tools:ignore="UselessParent,UseCompoundDrawables"> + + <!-- An icon shown on top of the contents. --> + <com.android.car.libraries.templates.host.view.widgets.common.CarImageView + android:id="@+id/voice_button" + android:layout_width="?templateLargeImageSize" + android:layout_height="?templateLargeImageSize" + app:imageMinWidth="?templateLargeImageSizeMin" + app:imageMaxWidth="?templateLargeImageSizeMax" + app:imageMinHeight="?templateLargeImageSizeMin" + app:imageMaxHeight="?templateLargeImageSizeMax" + tools:ignore="ContentDescription"/> + + <!-- The title displayed below the icon. --> + <CarUiTextView + android:id="@+id/state_description" + style="?templateMessageTitleTextStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginTop="?templateMessageTitleTopSpacing"/> + </LinearLayout> + </FrameLayout> +</LinearLayout> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/MapsTemplatePresenterFactory.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/MapsTemplatePresenterFactory.java new file mode 100644 index 0000000..7ab0e12 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/MapsTemplatePresenterFactory.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.presenters.maps; + +import android.content.Context; +import androidx.annotation.MainThread; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.car.app.model.PlaceListMapTemplate; +import androidx.car.app.model.Template; +import androidx.car.app.model.TemplateWrapper; +import androidx.lifecycle.Lifecycle.State; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.view.TemplatePresenter; +import com.android.car.libraries.apphost.view.TemplatePresenterFactory; +import com.android.car.libraries.apphost.view.widget.map.AbstractMapViewContainer; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.di.MapViewContainerFactory; +import com.google.common.collect.ImmutableSet; +import dagger.hilt.android.scopes.ServiceScoped; +import java.util.Collection; +import javax.inject.Inject; + +/** + * Implementation of a {@link TemplatePresenterFactory} for the production host, responsible for + * providing {@link TemplatePresenter} instances for the set of templates the host supports. + */ +@ServiceScoped +public class MapsTemplatePresenterFactory implements TemplatePresenterFactory { + + private static final ImmutableSet<Class<? extends Template>> SUPPORTED_TEMPLATES = + ImmutableSet.of(PlaceListMapTemplate.class); + + /** Boolean to trigger an one-time preload of MapView rendering code. */ + private static boolean sMapViewPreloaded; + + private final MapViewContainerFactory mMapViewContainerFactory; + + @Inject + MapsTemplatePresenterFactory(MapViewContainerFactory mapViewContainerFactory) { + mMapViewContainerFactory = mapViewContainerFactory; + } + + @VisibleForTesting + public static boolean isMapViewPreloaded() { + return sMapViewPreloaded; + } + + /** + * Performance optimization: preload some of the MapView rendering code that requires one-time + * static initialization. + * + * <p>This helps to speed up MapView being loaded when the {@link PlaceListMapTemplate} is + * actually used. + */ + @MainThread + public void preloadMapView(Context context) { + if (sMapViewPreloaded) { + L.d(LogTags.TEMPLATE, "MapView previously preloaded. Skipping."); + return; + } + + AbstractMapViewContainer mapViewContainer = + mMapViewContainerFactory.create(context, R.style.Theme_Template); + // onCreate triggers the mapView.getMapAsync call to initialize the mapView. + mapViewContainer.getLifecycleRegistry().setCurrentState(State.CREATED); + // make sure the mapView is properly cleaned up to avoid any possible leaks. + mapViewContainer.getLifecycleRegistry().setCurrentState(State.DESTROYED); + sMapViewPreloaded = true; + } + + @Override + @Nullable + public TemplatePresenter createPresenter( + TemplateContext templateContext, TemplateWrapper templateWrapper) { + Class<? extends Template> clazz = templateWrapper.getTemplate().getClass(); + + if (PlaceListMapTemplate.class == clazz) { + return PlaceListMapTemplatePresenter.create( + templateContext, templateWrapper, mMapViewContainerFactory); + } else { + L.w( + LogTags.TEMPLATE, + "Don't know how to create a presenter for template: %s", + clazz.getSimpleName()); + } + return null; + } + + @Override + public Collection<Class<? extends Template>> getSupportedTemplates() { + return SUPPORTED_TEMPLATES; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/PlaceListMapTemplatePresenter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/PlaceListMapTemplatePresenter.java new file mode 100644 index 0000000..a5205c6 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/PlaceListMapTemplatePresenter.java @@ -0,0 +1,334 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.presenters.maps; + +import static android.view.View.VISIBLE; +import static java.util.Objects.requireNonNull; + +import android.annotation.SuppressLint; +import android.graphics.Rect; +import android.transition.TransitionInflater; +import android.transition.TransitionManager; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.car.app.model.ActionStrip; +import androidx.car.app.model.ItemList; +import androidx.car.app.model.PlaceListMapTemplate; +import androidx.car.app.model.TemplateWrapper; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.Lifecycle.State; +import com.android.car.libraries.apphost.common.EventManager; +import com.android.car.libraries.apphost.common.EventManager.EventType; +import com.android.car.libraries.apphost.common.LocationMediator; +import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints; +import com.android.car.libraries.apphost.distraction.constraints.RowListConstraints; +import com.android.car.libraries.apphost.template.view.model.RowListWrapper; +import com.android.car.libraries.apphost.template.view.model.RowWrapper; +import com.android.car.libraries.apphost.view.AbstractTemplatePresenter; +import com.android.car.libraries.apphost.view.TemplatePresenter; +import com.android.car.libraries.apphost.view.widget.map.AbstractMapViewContainer; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.di.MapViewContainerFactory; +import com.android.car.libraries.templates.host.view.widgets.common.ActionStripView; +import com.android.car.libraries.templates.host.view.widgets.common.CardHeaderView; +import com.android.car.libraries.templates.host.view.widgets.common.ContentView; +import com.google.common.collect.ImmutableList; + +/** A {@link TemplatePresenter} that shows a map view with pins for locations. */ +public class PlaceListMapTemplatePresenter extends AbstractTemplatePresenter { + private final ViewGroup mRootView; + private final ViewGroup mCardContainer; + private final ActionStripView mActionStripView; + private final CardHeaderView mHeaderView; + private final ContentView mContentView; + // This is lazy-initiated during every onStart instead of just in the ctor. For some reason the + // map view is not laid out when the user exits the app and comes back to the template, + // preventing the map from updating from place changes. Therefore as a workaround we just + // re-create the map evertime the user comes back. See b/178606261 for more details. + @Nullable private AbstractMapViewContainer mMapContainer; + private final OnGlobalLayoutListener mGlobalLayoutListener; + private final MapViewContainerFactory mMapViewContainerFactory; + + /** Creates a {@link PlaceListMapTemplatePresenter}. */ + public static PlaceListMapTemplatePresenter create( + TemplateContext templateContext, + TemplateWrapper templateWrapper, + MapViewContainerFactory mapViewContainerFactory) { + PlaceListMapTemplatePresenter presenter = + new PlaceListMapTemplatePresenter( + templateContext, templateWrapper, mapViewContainerFactory); + presenter.update(); + return presenter; + } + + @VisibleForTesting + @Nullable + public AbstractMapViewContainer getMapContainer() { + return mMapContainer; + } + + @Override + public void onCreate() { + super.onCreate(); + updateMapContainerLifeCycle(State.CREATED); + } + + @Override + public void onDestroy() { + updateMapContainerLifeCycle(State.DESTROYED); + super.onDestroy(); + } + + @Override + public void onStart() { + super.onStart(); + updateMapContainerLifeCycle(State.STARTED); + + EventManager eventManager = getTemplateContext().getEventManager(); + eventManager.subscribeEvent(this, EventType.CONFIGURATION_CHANGED, this::refreshViews); + eventManager.subscribeEvent(this, EventType.PLACE_LIST, this::updatePlaces); + + // Instantiating the map views during onStart as otherwise the map may not get laid out + // properly. See b/178606261 for more details. + refreshViews(); + } + + @Override + public void onStop() { + updateMapContainerLifeCycle(State.CREATED); + TemplateContext templateContext = getTemplateContext(); + + // Clear the list of places when transitioning out of this presenter. + // This prevents a flow when the app enters this presenter again, it will temporarily show + // the previous markers that were set. + requireNonNull(templateContext.getAppHostService(LocationMediator.class)) + .setCurrentPlaces(ImmutableList.of()); + + templateContext.getEventManager().unsubscribeEvent(this, EventType.CONFIGURATION_CHANGED); + templateContext.getEventManager().unsubscribeEvent(this, EventType.PLACE_LIST); + + super.onStop(); + } + + @Override + public void onPause() { + getView().getViewTreeObserver().removeOnGlobalLayoutListener(mGlobalLayoutListener); + updateMapContainerLifeCycle(State.STARTED); + super.onPause(); + } + + @Override + public void onResume() { + super.onResume(); + getView().getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener); + updateMapContainerLifeCycle(State.RESUMED); + } + + @Override + public View getView() { + return mRootView; + } + + @Override + protected View getDefaultFocusedView() { + if (mContentView.getVisibility() == VISIBLE) { + return mContentView; + } + return super.getDefaultFocusedView(); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent keyEvent) { + // Move between the card view and the action button view on left or right rotary nudge. + if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + // If the focus is in the card view (back button or row list), request focus in the + // action strip. + if (moveFocusIfPresent( + ImmutableList.of(mCardContainer), ImmutableList.of(mActionStripView))) { + return true; + } + } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { + // Request focus on the content view so that the first row in the list will take focus. + if (moveFocusIfPresent(ImmutableList.of(mActionStripView), ImmutableList.of(mContentView))) { + return true; + } + } + return super.onKeyUp(keyCode, keyEvent); + } + + @Override + public void onTemplateChanged() { + update(); + } + + @Override + public boolean handlesTemplateChangeAnimation() { + // PlaceListMapTemplate has special behavior since we don't want to destroy the MapView for + // a refresh, and it handles the update motion correctly internally. + return true; + } + + @Override + public boolean isFullScreen() { + return false; + } + + /** Updates the locations in the map */ + private void update() { + PlaceListMapTemplate mapTemplate = (PlaceListMapTemplate) getTemplate(); + TemplateContext templateContext = getTemplateContext(); + + if (mapTemplate.isLoading()) { + // Clear the last list of places if we are in loading state so that we are not showing + // stale markers that do not correspond to the current list. + requireNonNull(templateContext.getAppHostService(LocationMediator.class)) + .setCurrentPlaces(ImmutableList.of()); + } + + TransitionManager.beginDelayedTransition( + mRootView, + TransitionInflater.from(templateContext) + .inflateTransition(R.transition.map_template_transition)); + + mHeaderView.setContent( + templateContext, + mapTemplate.getTitle(), + mapTemplate.getHeaderAction(), + mapTemplate.getOnContentRefreshDelegate()); + + ItemList itemList = mapTemplate.getItemList(); + RowListWrapper rowListWrapper = + RowListWrapper.wrap(templateContext, itemList) + .setIsLoading(mapTemplate.isLoading()) + .setRowFlags(RowWrapper.DEFAULT_UNIFORM_LIST_ROW_FLAGS) + .setRowListConstraints(RowListConstraints.ROW_LIST_CONSTRAINTS_SIMPLE) + .setIsRefresh(getTemplateWrapper().isRefresh()) + .setIsHalfList(true) + .build(); + mContentView.setRowListContent(templateContext, rowListWrapper); + + updateMapSettings(mapTemplate); + updateActionStrip(mapTemplate.getActionStrip()); + } + + // TODO(b/159908673): add tests for the lifecycle management logic in here. + private void refreshViews() { + // Destroy the previous MapView based on this presenter's currently lifecycle events. + AbstractMapViewContainer previousMapContainer = mMapContainer; + if (previousMapContainer != null) { + previousMapContainer.getLifecycleRegistry().setCurrentState(State.DESTROYED); + mRootView.removeView(previousMapContainer); + } + + Lifecycle lifecycle = getLifecycle(); + if (lifecycle.getCurrentState() == State.DESTROYED) { + // View already destroyed. Don't bother refreshing the views. + return; + } + + mMapContainer = mMapViewContainerFactory.create(getTemplateContext(), R.style.Theme_Template); + + if (mMapContainer != null) { + mMapContainer.setTemplateContext(getTemplateContext()); + mMapContainer.setId(R.id.map_container); + mRootView.addView(mMapContainer, 0); + + // Update the new MapView's lifecycle events to match this presenter's, as that is + // required for the map instance to be initiated and shown. + mMapContainer.getLifecycleRegistry().setCurrentState(lifecycle.getCurrentState()); + } + + PlaceListMapTemplate mapTemplate = (PlaceListMapTemplate) getTemplate(); + updateMapSettings(mapTemplate); + updateActionStrip(mapTemplate.getActionStrip()); + } + + private void updateMapSettings(PlaceListMapTemplate mapTemplate) { + AbstractMapViewContainer container = mMapContainer; + if (container != null) { + container.setCurrentLocationEnabled(mapTemplate.isCurrentLocationEnabled()); + container.setAnchor(mapTemplate.getAnchor()); + updatePlaces(); + } + } + + private void updatePlaces() { + AbstractMapViewContainer container = mMapContainer; + if (container != null) { + container.setPlaces( + requireNonNull(getTemplateContext().getAppHostService(LocationMediator.class)) + .getCurrentPlaces()); + } + } + + private void updateActionStrip(@Nullable ActionStrip actionStrip) { + mActionStripView.setActionStrip( + getTemplateContext(), actionStrip, ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE); + } + + private void updateMapContainerLifeCycle(State state) { + AbstractMapViewContainer container = mMapContainer; + // TODO(b/180162594): Use ifNotNull when available. + if (container != null) { + container.getLifecycleRegistry().setCurrentState(state); + } + } + + @SuppressLint("InflateParams") + @SuppressWarnings({"methodref.receiver.bound.invalid", "nullness"}) + private PlaceListMapTemplatePresenter( + TemplateContext templateContext, + TemplateWrapper templateWrapper, + MapViewContainerFactory mapViewContainerFactory) { + super(templateContext, templateWrapper, StatusBarState.OVER_SURFACE); + + mRootView = + (ViewGroup) + LayoutInflater.from(templateContext).inflate(R.layout.map_template_layout, null); + mCardContainer = mRootView.findViewById(R.id.card_container); + mHeaderView = mRootView.findViewById(R.id.header_view); + mContentView = mRootView.findViewById(R.id.content_view); + mActionStripView = mRootView.findViewById(R.id.action_strip); + mMapViewContainerFactory = mapViewContainerFactory; + // Note that the map container is instantiated during onStart. + + // We should always show an ItemList. + mCardContainer.setVisibility(View.VISIBLE); + + // Dynamically update the visible area inset. This allows the MapViewContainer to account + // for the insets when adjusting zoom levels to show all the place markers. + mGlobalLayoutListener = + () -> { + Rect safeAreaInset = new Rect(); + // The content container is always visible so just use its right. + safeAreaInset.left = mCardContainer.getRight(); + safeAreaInset.top = + mActionStripView.getVisibility() == VISIBLE + ? mActionStripView.getBottom() + : mRootView.getTop() + mRootView.getPaddingTop(); + safeAreaInset.bottom = mRootView.getBottom() - mRootView.getPaddingBottom(); + safeAreaInset.right = mRootView.getRight() - mRootView.getPaddingRight(); + templateContext.getSurfaceInfoProvider().setVisibleArea(safeAreaInset); + }; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res/layout-w1280dp-h1000dp/map_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res/layout-w1280dp-h1000dp/map_template_layout.xml new file mode 100644 index 0000000..03aac7d --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res/layout-w1280dp-h1000dp/map_template_layout.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<FrameLayout + 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="match_parent"> + + <!-- + The MapViewContainer is added programmatically here. + On configuration changes, the MapView is removed and re-added so that the + map can instantiate in the correct day/night mode. + TODO(b/159348229): update this once MapView has an explicit API to do this. + --> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/content_container" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <include + android:id="@+id/card_container" + android:layout_width="?templateCardContentContainerDefaultWidth" + android:layout_height="0dp" + android:layout_marginStart="?templateCardContentContainerStartMargin" + android:layout_marginTop="?templateCardContentContainerTopMargin" + app:layout_goneMarginTop="?templateCardContentContainerTopMargin" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/action_strip" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintVertical_bias="0.0" + app:layout_constraintHeight_default="wrap" + app:layout_constraintHeight_min="?templateCardContentContainerMinHeight" + layout="@layout/card_container"/> + + <include + android:id="@+id/action_strip" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + layout="@layout/action_strip_view_floating"/> + </androidx.constraintlayout.widget.ConstraintLayout> +</FrameLayout> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res/layout/map_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res/layout/map_template_layout.xml new file mode 100644 index 0000000..fb60c0f --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res/layout/map_template_layout.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<FrameLayout + 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="match_parent"> + + <!-- + The MapViewContainer is added programmatically here. + On configuration changes, the MapView is removed and re-added so that the + map can instantiate in the correct day/night mode. + TODO(b/159348229): update this once MapView has an explicit API to do this. + --> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/content_container" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <include + android:id="@+id/card_container" + layout="@layout/card_container"/> + + <include + android:id="@+id/action_strip" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + layout="@layout/action_strip_view_floating"/> + </androidx.constraintlayout.widget.ConstraintLayout> +</FrameLayout> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res/transition/map_template_transition.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res/transition/map_template_transition.xml new file mode 100644 index 0000000..59e3103 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res/transition/map_template_transition.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<transitionSet xmlns:android="http://schemas.android.com/apk/res/android" + android:transitionOrdering="together"> + <targets> + <target android:excludeId="@id/map_container" /> + </targets> + <fade + android:fadingMode="fade_in_out" + android:duration="?templateUpdateAnimationDurationMilliseconds"/> + <changeBounds + android:duration="?templateUpdateAnimationDurationMilliseconds" + android:interpolator="@interpolator/fast_out_slow_in"/> +</transitionSet> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/NavigationTemplatePresenter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/NavigationTemplatePresenter.java new file mode 100644 index 0000000..fe6aebc --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/NavigationTemplatePresenter.java @@ -0,0 +1,695 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.presenters.navigation; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static com.android.car.libraries.templates.host.view.widgets.common.ActionStripView.ACTIONSTRIP_ACTIVE_STATE_DURATION_MILLIS; +import static java.lang.Math.max; +import static java.lang.Math.min; + +import android.annotation.SuppressLint; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.drawable.GradientDrawable; +import android.transition.TransitionInflater; +import android.transition.TransitionManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import android.view.ViewGroup.MarginLayoutParams; +import android.view.ViewTreeObserver.OnGlobalFocusChangeListener; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; +import android.widget.ImageView; +import androidx.annotation.ColorInt; +import androidx.annotation.StyleableRes; +import androidx.car.app.model.Action; +import androidx.car.app.model.ActionStrip; +import androidx.car.app.model.CarColor; +import androidx.car.app.model.CarText; +import androidx.car.app.model.TemplateWrapper; +import androidx.car.app.navigation.model.MessageInfo; +import androidx.car.app.navigation.model.NavigationTemplate; +import androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo; +import androidx.car.app.navigation.model.PanModeDelegate; +import androidx.car.app.navigation.model.RoutingInfo; +import androidx.car.app.navigation.model.TravelEstimate; +import com.android.car.libraries.apphost.common.CarColorUtils; +import com.android.car.libraries.apphost.common.EventManager; +import com.android.car.libraries.apphost.common.EventManager.EventType; +import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.common.ThreadUtils; +import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints; +import com.android.car.libraries.apphost.distraction.constraints.CarColorConstraints; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.template.view.model.ActionStripWrapper; +import com.android.car.libraries.apphost.view.AbstractSurfaceTemplatePresenter; +import com.android.car.libraries.apphost.view.TemplatePresenter; +import com.android.car.libraries.apphost.view.common.CarTextParams; +import com.android.car.libraries.apphost.view.common.ImageUtils; +import com.android.car.libraries.apphost.view.common.ImageViewParams; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.view.animation.AnimationListenerAdapter; +import com.android.car.libraries.templates.host.view.widgets.common.ActionStripView; +import com.android.car.libraries.templates.host.view.widgets.common.BleedingCardView; +import com.android.car.libraries.templates.host.view.widgets.common.PanOverlayView; +import com.android.car.libraries.templates.host.view.widgets.navigation.CompactStepView; +import com.android.car.libraries.templates.host.view.widgets.navigation.DetailedStepView; +import com.android.car.libraries.templates.host.view.widgets.navigation.MessageView; +import com.android.car.libraries.templates.host.view.widgets.navigation.ProgressView; +import com.android.car.libraries.templates.host.view.widgets.navigation.TravelEstimateView; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A {@link TemplatePresenter} that shows various navigation cards such as routing cards, + * destination cards etc. + */ +public class NavigationTemplatePresenter extends AbstractSurfaceTemplatePresenter + implements ActionStripView.ActiveStateDelegate { + private static final int MAX_IMAGES_PER_TEXT_LINE = 2; + + /** Percentage to lower the brightness of the card's background. */ + private static final float CARD_BACKGROUND_DARKEN_PERCENTAGE = 0.2f; + + /** Percentage to lower the brightness of the compact step section of the card's background. */ + private static final float COMPACT_STEP_CARD_BACKGROUND_DARKEN_PERCENTAGE = 0.4f; + + /** The ratio between the junction image max height to the routing card. */ + private static final float JUNCTION_IMAGE_MAX_HEIGHT_TO_CARD_WIDTH_RATIO = 0.625f; + + /** The ratio between the lanes image container height to the routing card. */ + private static final float LANES_IMAGE_CONTAINER_HEIGHT_TO_CARD_WIDTH_RATIO = 0.175f; + + /** + * {@link #showActionStripViews()} is called in {@link #onStart()}, but a bug in GMS core causes + * {@link View#isInTouchMode()} in {@link #onStart()} to return {@code true} even in rotary or + * touchpad mode (b/128031459), which prevents the action strip from taking the input focus. We + * use this listener to call {@link #showActionStripViews()} after the touch mode changes to the + * correct value. + */ + private final OnGlobalFocusChangeListener mOnGlobalFocusChangeListener = + new OnGlobalFocusChangeListener() { + // call to showActionStripView() not allowed on the given receiver. + @SuppressWarnings("nullness:method.invocation") + @Override + public void onGlobalFocusChanged(View oldFocus, View newFocus) { + if (newFocus != null) { + showActionStripViews(); + } + } + }; + + @ColorInt private final int mNavCardFallbackContentColor; + + private int mStepsCardContainerVisibility = GONE; + private int mTravelEstimateContainerVisibility = GONE; + + private final ViewGroup mRootView; + private final BleedingCardView mStepsCardContainer; + private final ViewGroup mStepsContainer; + private final MessageView mMessageView; + private final ProgressView mProgressView; + private final ViewGroup mTravelEstimateContainer; + private final ImageView mJunctionImageView; + private final FrameLayout mJunctionImageContainer; + private final FrameLayout mLanesImageContainerView; + private final DetailedStepView mDetailedStepView; + private final CompactStepView mCompactStepView; + private final TravelEstimateView mTravelEstimateView; + private final ActionStripView mActionStripView; + private final ActionStripView mMapActionStripView; + private final PanOverlayView mPanOverlay; + private final CarTextParams mCurrentStepParams; + private final CarTextParams mNextStepParams; + @ColorInt private final int mDefaultCardBackgroundColor; + + /** Creates a {@link NavigationTemplatePresenter}. */ + public static NavigationTemplatePresenter create( + TemplateContext templateContext, TemplateWrapper templateWrapper) { + NavigationTemplatePresenter presenter = + new NavigationTemplatePresenter(templateContext, templateWrapper); + presenter.update(); + return presenter; + } + + @Override + public void onStart() { + super.onStart(); + + showActionStripViews(); + EventManager eventManager = getTemplateContext().getEventManager(); + eventManager.subscribeEvent( + this, EventType.TEMPLATE_TOUCHED_OR_FOCUSED, this::showActionStripViews); + eventManager.subscribeEvent(this, EventType.WINDOW_FOCUS_CHANGED, this::showActionStripViews); + eventManager.subscribeEvent( + this, + EventType.CONFIGURATION_CHANGED, + () -> { + NavigationTemplate template = (NavigationTemplate) getTemplate(); + updateActionStrip(template.getActionStrip()); + updateMapActionStrip(template.getMapActionStrip()); + wrapActionStripsIfNeeded(); + }); + getView().getViewTreeObserver().addOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener); + getTemplateContext().getRoutingInfoState().setIsRoutingInfoVisible(true); + } + + @Override + public void onStop() { + getTemplateContext().getRoutingInfoState().setIsRoutingInfoVisible(false); + EventManager eventManager = getTemplateContext().getEventManager(); + eventManager.unsubscribeEvent(this, EventType.TEMPLATE_TOUCHED_OR_FOCUSED); + eventManager.unsubscribeEvent(this, EventType.WINDOW_FOCUS_CHANGED); + eventManager.unsubscribeEvent(this, EventType.CONFIGURATION_CHANGED); + getView().getViewTreeObserver().removeOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener); + + super.onStop(); + } + + @Override + public View getView() { + return mRootView; + } + + @Override + public void onTemplateChanged() { + update(); + } + + @Override + public boolean isPanAndZoomEnabled() { + return getTemplateContext().getCarHostConfig().isNavPanZoomEnabled(); + } + + @Override + public void onPanModeChanged(boolean isInPanMode) { + showActionStripViews(); + updateVisibility(isInPanMode); + dispatchPanModeChange(isInPanMode); + } + + @Override + protected View getDefaultFocusedView() { + // Check the action strip visibility because the action buttons can take focus even when the + // action strip is gone. + if (mActionStripView.getVisibility() == VISIBLE) { + return mActionStripView; + } + if (mMapActionStripView.getVisibility() == VISIBLE) { + return mMapActionStripView; + } + return super.getDefaultFocusedView(); + } + + @Override + public void calculateAdditionalInset(Rect inset) { + // The portrait inset is more favorable to portrait screens it is calculated as the following + // bounding box: + // * left: inset left + // * right: Min(inset right, mapActionStrip left) + // * top: Max(inset top, actionStrip bottom, steps card bottom) + // * bottom: Min(inset bottom, travelEstimateContainer) + Rect portraitScreenInset = new Rect(inset); + + // The landscape inset is more favorable to landscape screens it is calculated as the following + // bounding box: + // * left: Max(inset left, travelEstimateContainer, stepsContainer) + // * right: Min(inset right, mapActionStrip left) + // * top: Max(inset top, actionStrip bottom) + // * bottom: inset bottom + Rect landscapeScreenInset = new Rect(inset); + + if (mMapActionStripView.getVisibility() == View.VISIBLE) { + landscapeScreenInset.right = min(landscapeScreenInset.right, mMapActionStripView.getLeft()); + portraitScreenInset.right = min(portraitScreenInset.right, mMapActionStripView.getLeft()); + } + if (mActionStripView.getVisibility() == VISIBLE) { + landscapeScreenInset.top = max(landscapeScreenInset.top, mActionStripView.getBottom()); + portraitScreenInset.top = max(portraitScreenInset.top, mActionStripView.getBottom()); + } + if (mTravelEstimateContainerVisibility == VISIBLE) { + portraitScreenInset.bottom = + min(portraitScreenInset.bottom, mTravelEstimateContainer.getTop()); + landscapeScreenInset.left = + max(landscapeScreenInset.left, mTravelEstimateContainer.getRight()); + } + if (mStepsCardContainerVisibility == View.VISIBLE) { + landscapeScreenInset.left = max(landscapeScreenInset.left, mStepsCardContainer.getRight()); + portraitScreenInset.top = max(portraitScreenInset.top, mStepsCardContainer.getBottom()); + } + int landscapeScreenArea = landscapeScreenInset.height() * landscapeScreenInset.width(); + int portraitScreenArea = portraitScreenInset.height() * portraitScreenInset.width(); + inset.set( + landscapeScreenArea > portraitScreenArea ? landscapeScreenInset : portraitScreenInset); + } + + @Override + public void onActiveStateVisibilityChanged() { + requestVisibleAreaUpdate(); + } + + private void update() { + NavigationTemplate template = (NavigationTemplate) getTemplate(); + updateActionStrip(template.getActionStrip()); + updateMapActionStrip(template.getMapActionStrip()); + getPanZoomManager().setEnabled(hasPanButton()); + setStepsCardBackgroundColor(); + setStepsCardContentColor(); + + TransitionManager.beginDelayedTransition( + mRootView, + TransitionInflater.from(getTemplateContext()) + .inflateTransition(R.transition.routing_card_transition)); + + @ColorInt int cardBackgroundColor = mStepsCardContainer.getCardBackgroundColor(); + boolean shouldHideTravelEstimate = false; + NavigationInfo navigationInfo = template.getNavigationInfo(); + if (navigationInfo == null) { + mStepsCardContainerVisibility = GONE; + mProgressView.setVisibility(GONE); + mStepsContainer.setVisibility(GONE); + mMessageView.setVisibility(GONE); + } else if (navigationInfo instanceof RoutingInfo) { + RoutingInfo routingInfo = (RoutingInfo) navigationInfo; + + if (routingInfo.isLoading()) { + mStepsCardContainerVisibility = VISIBLE; + mProgressView.setVisibility(VISIBLE); + mStepsContainer.setVisibility(GONE); + mMessageView.setVisibility(GONE); + } else { + + boolean shouldShowJunctionImage = + ImageUtils.setImageSrc( + getTemplateContext(), + routingInfo.getJunctionImage(), + mJunctionImageView, + ImageViewParams.DEFAULT); + + boolean shouldShowNextStep = routingInfo.getNextStep() != null; + + mDetailedStepView.setStepAndDistance( + getTemplateContext(), + routingInfo.getCurrentStep(), + routingInfo.getCurrentDistance(), + mCurrentStepParams, + cardBackgroundColor, + shouldShowJunctionImage); + mCompactStepView.setStep( + getTemplateContext(), routingInfo.getNextStep(), mNextStepParams, cardBackgroundColor); + + if (shouldShowJunctionImage) { + mJunctionImageContainer.setVisibility(VISIBLE); + mCompactStepView.setVisibility(GONE); + shouldHideTravelEstimate = true; + } else { + mJunctionImageContainer.setVisibility(GONE); + mCompactStepView.setVisibility(shouldShowNextStep ? VISIBLE : GONE); + } + + boolean hasNextStepOrJunction = shouldShowJunctionImage || shouldShowNextStep; + mStepsCardContainer + .findViewById(R.id.divider) + .setVisibility(hasNextStepOrJunction ? VISIBLE : GONE); + + mStepsCardContainerVisibility = VISIBLE; + mProgressView.setVisibility(GONE); + mStepsContainer.setVisibility(VISIBLE); + mMessageView.setVisibility(GONE); + } + } else if (navigationInfo instanceof MessageInfo) { + MessageInfo messageInfo = (MessageInfo) navigationInfo; + CarText title = messageInfo.getTitle(); + if (title == null) { + L.w(LogTags.TEMPLATE, "Title for the message is expected but not set"); + title = CarText.create(""); + } + mMessageView.setMessage( + getTemplateContext(), + messageInfo.getImage(), + title, + messageInfo.getText(), + cardBackgroundColor); + mStepsCardContainerVisibility = VISIBLE; + mProgressView.setVisibility(GONE); + mStepsContainer.setVisibility(GONE); + mMessageView.setVisibility(VISIBLE); + } else { + L.w(LogTags.TEMPLATE, "Unknown navigation info: %s", navigationInfo); + } + + TravelEstimate travelEstimate = template.getDestinationTravelEstimate(); + if (travelEstimate == null || shouldHideTravelEstimate) { + mTravelEstimateContainerVisibility = GONE; + } else { + mTravelEstimateView.setTravelEstimate(getTemplateContext(), travelEstimate); + mTravelEstimateContainerVisibility = VISIBLE; + } + + updateVisibility(getPanZoomManager().isInPanMode()); + + // Wrap action strips after the visibility update, because we need to know if the routing + // card is visible in order to decide whether the action strips need to be wrapped. + wrapActionStripsIfNeeded(); + + getTemplateContext().getSurfaceInfoProvider().invalidateStableArea(); + requestVisibleAreaUpdate(); + } + + /** + * Navigation template allows up to 4 buttons, which may overlap with the routing card container + * in small screens. In this case, draw the buttons in 2 lines to avoid the overlap. + */ + // TODO(b/191828230): Determine the action strip overlaps properly + private void wrapActionStripsIfNeeded() { + ThreadUtils.runOnMain( + () -> { + NavigationTemplate template = (NavigationTemplate) getTemplate(); + int screenWidth = getTemplateContext().getResources().getDisplayMetrics().widthPixels; + int screenHeight = getTemplateContext().getResources().getDisplayMetrics().heightPixels; + + // Measure and layout manually to get the correct view widths. + mRootView.measure( + MeasureSpec.makeMeasureSpec(screenWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(screenHeight, MeasureSpec.EXACTLY)); + mRootView.layout(0, 0, screenWidth, screenHeight); + + // We calculate the right side of the card container and the left side of the + // action strip because the manual measure and layout calls do not produce the + // correct view position in the window. + MarginLayoutParams stepsCardContainerLayoutParams = + (MarginLayoutParams) mStepsCardContainer.getLayoutParams(); + int stepsCardContainerRight = + stepsCardContainerLayoutParams.getMarginStart() + + stepsCardContainerLayoutParams.width; + int actionStripViewLeft = screenWidth - mActionStripView.getWidth(); + + // If the card container and the action strip view overlap, draw the action + // strip in 2 lines to avoid the overlap. + if (mStepsCardContainer.getVisibility() == VISIBLE + && mActionStripView.getVisibility() == VISIBLE + && stepsCardContainerRight > actionStripViewLeft) { + updateActionStrip(template.getActionStrip(), /* allowTwoLines= */ true); + } + + // We calculate the bottom side of the action strip and the top side of the map + // action strip because the manual measure and layout calls do not produce the + // correct view position in the window. + int actionStripViewBottom = mActionStripView.getBottom(); + int mapActionStripViewTop = mMapActionStripView.getTop(); + + // If the action strip and the map action strip views overlap, draw the map + // action strip in 2 lines to avoid the overlap. + if (mActionStripView.getVisibility() == VISIBLE + && mMapActionStripView.getVisibility() == VISIBLE + && actionStripViewBottom > mapActionStripViewTop) { + updateMapActionStrip(template.getMapActionStrip(), /* allowTwoLines= */ true); + } + }); + } + + private void updateActionStrip(@Nullable ActionStrip actionStrip) { + updateActionStrip(actionStrip, /* allowTwoLines= */ false); + } + + private void updateActionStrip(@Nullable ActionStrip actionStrip, boolean allowTwoLines) { + mActionStripView.setActionStrip( + getTemplateContext(), + actionStrip, + ActionsConstraints.ACTIONS_CONSTRAINTS_NAVIGATION, + allowTwoLines); + } + + private void updateMapActionStrip(@Nullable ActionStrip actionStrip) { + updateMapActionStrip(actionStrip, /* allowTwoLines= */ false); + } + + private void updateMapActionStrip(@Nullable ActionStrip actionStrip, boolean allowTwoLines) { + ActionStripWrapper actionStripWrapper = null; + if (actionStrip != null) { + actionStripWrapper = + getPanZoomManager() + .getMapActionStripWrapper( + /* templateContext= */ getTemplateContext(), /* actionStrip= */ actionStrip); + } + + mMapActionStripView.setActionStrip( + getTemplateContext(), + actionStripWrapper, + ActionsConstraints.ACTIONS_CONSTRAINTS_NAVIGATION_MAP, + allowTwoLines); + } + + private void showActionStripViews() { + boolean isInPanMode = getPanZoomManager().isInPanMode(); + + // Show the action strip when not in the pan mode. + mActionStripView.setActiveState(!isInPanMode); + mMapActionStripView.setActiveState(true); + + // If nothing was focused, set the default focus. + if (!mRootView.hasFocus()) { + setDefaultFocus(); + } + + // The action strip view should fade if the action strip or the window is not focused. + if (!(mActionStripView.hasFocus() || mMapActionStripView.hasFocus()) || !hasWindowFocus()) { + mActionStripView.setActiveStateWithDelay(false, ACTIONSTRIP_ACTIVE_STATE_DURATION_MILLIS); + + // Fade the map action strip only when not in the pan mode. + if (!isInPanMode) { + mMapActionStripView.setActiveStateWithDelay( + false, ACTIONSTRIP_ACTIVE_STATE_DURATION_MILLIS); + } + } + } + + private void attachActiveStateDelegate() { + mActionStripView.setActiveStateDelegate(this); + mMapActionStripView.setActiveStateDelegate(this); + } + + @SuppressLint("InflateParams") + @SuppressWarnings("nullness:method.invocation") + private NavigationTemplatePresenter( + TemplateContext templateContext, TemplateWrapper templateWrapper) { + super(templateContext, templateWrapper, StatusBarState.OVER_SURFACE); + + // Read the fallback color to use with the app-defined card background color. + @StyleableRes final int[] themeAttrs = {R.attr.templateNavCardFallbackContentColor}; + TypedArray ta = templateContext.obtainStyledAttributes(themeAttrs); + mNavCardFallbackContentColor = ta.getColor(0, Color.WHITE); + ta.recycle(); + + mRootView = + (ViewGroup) + LayoutInflater.from(templateContext).inflate(R.layout.navigation_template_layout, null); + mStepsCardContainer = mRootView.findViewById(R.id.content_container); + mMessageView = mRootView.findViewById(R.id.message_view); + mProgressView = mRootView.findViewById(R.id.progress_view); + mStepsContainer = mRootView.findViewById(R.id.steps_container); + + mJunctionImageContainer = mRootView.findViewById(R.id.junction_image_container); + mJunctionImageView = mRootView.findViewById(R.id.junction_image); + mLanesImageContainerView = mRootView.findViewById(R.id.lanes_image_container); + mDetailedStepView = mRootView.findViewById(R.id.detailed_step_view); + mCompactStepView = mRootView.findViewById(R.id.compact_step_view); + mTravelEstimateContainer = mRootView.findViewById(R.id.travel_estimate_card_container); + mTravelEstimateView = mRootView.findViewById(R.id.travel_estimate_view); + mActionStripView = mRootView.findViewById(R.id.action_strip); + mMapActionStripView = mRootView.findViewById(R.id.map_action_strip); + mPanOverlay = mRootView.findViewById(R.id.pan_overlay); + + mCurrentStepParams = createStepTextParams(/* isNextStep= */ false); + mNextStepParams = createStepTextParams(/* isNextStep= */ true); + mDefaultCardBackgroundColor = mStepsCardContainer.getCardBackgroundColor(); + + setStepsCardBackgroundColor(); + + // Set the junction image max height and lanes image container height. + setJunctionImageMaxHeight(); + setLanesImageContainerHeight(); + + attachActiveStateDelegate(); + } + + /** + * Returns a {@link CarTextParams} instance to use for the text of a step. + * + * <p>Unlike other text elsewhere, image spans are allowed in these strings. + */ + @SuppressLint("ResourceType") + private CarTextParams createStepTextParams(boolean isNextStep) { + TemplateContext templateContext = getTemplateContext(); + + @StyleableRes + final int[] themeAttrs = { + R.attr.templateRoutingImageSpanRatio, + R.attr.templateRoutingImageSpanBody2MaxHeight, + R.attr.templateRoutingImageSpanBody3MaxHeight, + }; + TypedArray ta = templateContext.obtainStyledAttributes(themeAttrs); + float imageRatio = ta.getFloat(0, 0.f); + int body2MaxHeight = ta.getDimensionPixelSize(1, 0); + int body3MaxHeight = ta.getDimensionPixelSize(2, 0); + ta.recycle(); + + int maxHeight = isNextStep ? body3MaxHeight : body2MaxHeight; + int maxWidth = (int) (maxHeight * imageRatio); + return CarTextParams.builder() + .setImageBoundingBox(new Rect(0, 0, maxWidth, maxHeight)) + .setMaxImages(MAX_IMAGES_PER_TEXT_LINE) + .setColorSpanConstraints(CarColorConstraints.NO_COLOR) + .build(); + } + + private void showTravelEstimateContainer() { + if (mTravelEstimateContainer.getVisibility() == VISIBLE) { + return; + } + + mTravelEstimateContainer.setVisibility(VISIBLE); + Animation animation = + AnimationUtils.loadAnimation( + getTemplateContext(), R.anim.travel_estimate_card_show_animation); + mTravelEstimateContainer.setAnimation(animation); + } + + private void hideTravelEstimateContainer() { + if (mTravelEstimateContainer.getVisibility() == GONE) { + return; + } + Animation animation = + AnimationUtils.loadAnimation( + getTemplateContext(), R.anim.travel_estimate_card_hide_animation); + // TODO(b/180455232): Create default AnimationListenerListener with empty methods. + animation.setAnimationListener( + new AnimationListenerAdapter() { + @Override + public void onAnimationEnd(Animation animation) { + mTravelEstimateContainer.setVisibility(GONE); + } + }); + mTravelEstimateContainer.setAnimation(animation); + } + + private void setStepsCardBackgroundColor() { + // Set the card's background color to the one provided in the template, if any. + CarColor backgroundColor = ((NavigationTemplate) getTemplate()).getBackgroundColor(); + @ColorInt int backgroundColorInt; + if (backgroundColor != null) { + backgroundColorInt = + CarColorUtils.resolveColor( + getTemplateContext(), + backgroundColor, + false, + Color.BLACK, + CarColorConstraints.UNCONSTRAINED); + } else { + backgroundColorInt = mDefaultCardBackgroundColor; + } + + // Darken the background of the card. + mStepsCardContainer.setCardBackgroundColor( + CarColorUtils.darkenColor(backgroundColorInt, CARD_BACKGROUND_DARKEN_PERCENTAGE)); + + // Darken the background of the compat step view. + // We also create a drawable for it that has bottom rounded corners because otherwise the + // background of the card won't clip within the parent's outline. It is probably possible to + // do the clipping using a convex path (getting the card's background outline and using that + // does not work as it returns a rect and not a path), and setting it through an outline + // provider but this is cheaper regardless as clipping is an expensive operation. + float bottomRadius = mStepsCardContainer.getCardRadius(); + GradientDrawable drawable = new GradientDrawable(); + drawable.setCornerRadii( + new float[] {0, 0, 0, 0, bottomRadius, bottomRadius, bottomRadius, bottomRadius}); + drawable.setColor( + CarColorUtils.darkenColor( + backgroundColorInt, COMPACT_STEP_CARD_BACKGROUND_DARKEN_PERCENTAGE)); + mCompactStepView.setBackground(drawable); + } + + private void setStepsCardContentColor() { + if (((NavigationTemplate) getTemplate()).getBackgroundColor() != null) { + // Use the fallback content color if the app-defined card background color is used, + // because the OEM-defined text color may not have the adequate contrast ratio with the + // card background color. + mDetailedStepView.setTextColor(mNavCardFallbackContentColor); + mCompactStepView.setTextColor(mNavCardFallbackContentColor); + mMessageView.setTextColor(mNavCardFallbackContentColor); + mProgressView.setColor(mNavCardFallbackContentColor); + } else { + mDetailedStepView.setDefaultTextColor(); + mCompactStepView.setDefaultTextColor(); + mMessageView.setDefaultTextColor(); + mProgressView.setDefaultColor(); + } + } + + private void setJunctionImageMaxHeight() { + int stepsCardContainerWidth = mStepsCardContainer.getLayoutParams().width; + int junctionImageMaxHeight = + (int) (stepsCardContainerWidth * JUNCTION_IMAGE_MAX_HEIGHT_TO_CARD_WIDTH_RATIO); + mJunctionImageView.setMaxHeight(junctionImageMaxHeight); + } + + private void setLanesImageContainerHeight() { + int stepsCardContainerWidth = mStepsCardContainer.getLayoutParams().width; + int lanesImageContainerHeight = + (int) (stepsCardContainerWidth * LANES_IMAGE_CONTAINER_HEIGHT_TO_CARD_WIDTH_RATIO); + mLanesImageContainerView.getLayoutParams().height = lanesImageContainerHeight; + } + + private boolean hasPanButton() { + NavigationTemplate template = (NavigationTemplate) getTemplate(); + ActionStrip mapActionStrip = template.getMapActionStrip(); + return mapActionStrip != null && mapActionStrip.getFirstActionOfType(Action.TYPE_PAN) != null; + } + + private void dispatchPanModeChange(boolean isInPanMode) { + NavigationTemplate template = (NavigationTemplate) getTemplate(); + PanModeDelegate panModeDelegate = template.getPanModeDelegate(); + if (panModeDelegate != null) { + getTemplateContext().getAppDispatcher().dispatchPanModeChanged(panModeDelegate, isInPanMode); + } + } + + private void updateVisibility(boolean isInPanMode) { + ThreadUtils.runOnMain( + () -> { + if (isInPanMode) { + mPanOverlay.setVisibility(VISIBLE); + mStepsCardContainer.setVisibility(GONE); + mActionStripView.setActiveState(false); + hideTravelEstimateContainer(); + } else { + mPanOverlay.setVisibility(GONE); + mStepsCardContainer.setVisibility(mStepsCardContainerVisibility); + if (mTravelEstimateContainerVisibility == VISIBLE) { + showTravelEstimateContainer(); + } else { + hideTravelEstimateContainer(); + } + } + }); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/NavigationTemplatePresenterFactory.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/NavigationTemplatePresenterFactory.java new file mode 100644 index 0000000..7fd88e1 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/NavigationTemplatePresenterFactory.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.presenters.navigation; + +import androidx.car.app.model.Template; +import androidx.car.app.model.TemplateWrapper; +import androidx.car.app.navigation.model.NavigationTemplate; +import androidx.car.app.navigation.model.PlaceListNavigationTemplate; +import androidx.car.app.navigation.model.RoutePreviewNavigationTemplate; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.view.TemplatePresenter; +import com.android.car.libraries.apphost.view.TemplatePresenterFactory; +import com.google.common.collect.ImmutableSet; +import java.util.Collection; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Implementation of a {@link TemplatePresenterFactory} for the production host, responsible for + * providing {@link TemplatePresenter} instances for the set of templates the host supports. + */ +public class NavigationTemplatePresenterFactory implements TemplatePresenterFactory { + private static final NavigationTemplatePresenterFactory sInstance = + new NavigationTemplatePresenterFactory(); + private static final ImmutableSet<Class<? extends Template>> SUPPORTED_TEMPLATES = + ImmutableSet.of( + NavigationTemplate.class, + PlaceListNavigationTemplate.class, + RoutePreviewNavigationTemplate.class); + + /** Gets the singleton instance of{@link NavigationTemplatePresenterFactory}. */ + public static NavigationTemplatePresenterFactory get() { + return sInstance; + } + + @Override + @Nullable + public TemplatePresenter createPresenter( + TemplateContext templateContext, TemplateWrapper templateWrapper) { + Template template = templateWrapper.getTemplate(); + + Class<? extends Template> clazz = template.getClass(); + if (NavigationTemplate.class == clazz) { + return NavigationTemplatePresenter.create(templateContext, templateWrapper); + } else if (PlaceListNavigationTemplate.class == clazz) { + return PlaceListNavigationTemplatePresenter.create(templateContext, templateWrapper); + } else if (RoutePreviewNavigationTemplate.class == clazz) { + return RoutePreviewNavigationTemplatePresenter.create(templateContext, templateWrapper); + } else { + L.w( + LogTags.TEMPLATE, + "Don't know how to create a presenter for template: %s", + clazz.getSimpleName()); + } + return null; + } + + @Override + public Collection<Class<? extends Template>> getSupportedTemplates() { + return SUPPORTED_TEMPLATES; + } + + private NavigationTemplatePresenterFactory() {} +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/PlaceListNavigationTemplatePresenter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/PlaceListNavigationTemplatePresenter.java new file mode 100644 index 0000000..0f25c8c --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/PlaceListNavigationTemplatePresenter.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.presenters.navigation; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static com.android.car.libraries.apphost.distraction.constraints.RowListConstraints.ROW_LIST_CONSTRAINTS_SIMPLE; +import static java.lang.Math.max; +import static java.lang.Math.min; + +import android.annotation.SuppressLint; +import android.graphics.Rect; +import android.transition.TransitionInflater; +import android.transition.TransitionManager; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.car.app.model.Action; +import androidx.car.app.model.ActionStrip; +import androidx.car.app.model.ItemList; +import androidx.car.app.model.TemplateWrapper; +import androidx.car.app.navigation.model.PanModeDelegate; +import androidx.car.app.navigation.model.PlaceListNavigationTemplate; +import com.android.car.libraries.apphost.common.EventManager.EventType; +import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.common.ThreadUtils; +import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints; +import com.android.car.libraries.apphost.template.view.model.ActionStripWrapper; +import com.android.car.libraries.apphost.template.view.model.RowListWrapper; +import com.android.car.libraries.apphost.template.view.model.RowWrapper; +import com.android.car.libraries.apphost.view.AbstractSurfaceTemplatePresenter; +import com.android.car.libraries.apphost.view.TemplatePresenter; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.view.widgets.common.ActionStripView; +import com.android.car.libraries.templates.host.view.widgets.common.CardHeaderView; +import com.android.car.libraries.templates.host.view.widgets.common.ContentView; +import com.android.car.libraries.templates.host.view.widgets.common.PanOverlayView; +import com.google.common.collect.ImmutableList; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** A {@link TemplatePresenter} that shows a {@link PlaceListNavigationTemplate}. */ +public class PlaceListNavigationTemplatePresenter extends AbstractSurfaceTemplatePresenter { + private final ViewGroup mRootView; + private final ViewGroup mContentContainer; + private final CardHeaderView mHeaderView; + private final ContentView mContentView; + private final ActionStripView mActionStripView; + private final ActionStripView mMapActionStripView; + private final PanOverlayView mPanOverlay; + + /** Creates a {@link PlaceListNavigationTemplatePresenter}. */ + public static PlaceListNavigationTemplatePresenter create( + TemplateContext templateContext, TemplateWrapper templateWrapper) { + PlaceListNavigationTemplatePresenter presenter = + new PlaceListNavigationTemplatePresenter(templateContext, templateWrapper); + presenter.update(); + return presenter; + } + + @Override + public View getView() { + return mRootView; + } + + @Override + public void onStart() { + super.onStart(); + + getTemplateContext() + .getEventManager() + .subscribeEvent( + this, + EventType.CONFIGURATION_CHANGED, + () -> { + PlaceListNavigationTemplate template = (PlaceListNavigationTemplate) getTemplate(); + updateActionStrip(template.getActionStrip()); + updateMapActionStrip(template.getActionStrip()); + }); + } + + @Override + public void onStop() { + getTemplateContext().getEventManager().unsubscribeEvent(this, EventType.CONFIGURATION_CHANGED); + + super.onStop(); + } + + @Override + public void onTemplateChanged() { + update(); + } + + @Override + public boolean isPanAndZoomEnabled() { + return getTemplateContext().getCarHostConfig().isPoiRoutePreviewPanZoomEnabled(); + } + + @Override + public void onPanModeChanged(boolean isInPanMode) { + updateVisibility(isInPanMode); + dispatchPanModeChange(isInPanMode); + } + + @Override + protected View getDefaultFocusedView() { + if (mContentView.getVisibility() == VISIBLE) { + return mContentView; + } + return super.getDefaultFocusedView(); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent keyEvent) { + if (getPanZoomManager().handlePanEventsIfNeeded(keyCode)) { + return true; + } + + // Move between the card view and the action button view on left or right rotary nudge. + if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + // If the focus is in the card view (back button or row list), request focus in the + // action + // strip. + if (moveFocusIfPresent( + ImmutableList.of(mContentContainer), ImmutableList.of(mActionStripView))) { + return true; + } + } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { + // Request focus on the content view so that the first row in the list will take focus. + if (moveFocusIfPresent(ImmutableList.of(mActionStripView), ImmutableList.of(mContentView))) { + return true; + } + } + return super.onKeyUp(keyCode, keyEvent); + } + + @Override + public void calculateAdditionalInset(Rect inset) { + // The portrait inset is more favorable to portrait screens and is calculated as the following + // bounding box: + // * left: inset left + // * right: inset right + // * top: Max(inset top, actionStrip bottom, content view bottom) + // * bottom: inset bottom + Rect portraitScreenInset = new Rect(inset); + + // The landscape inset is more favorable to landscape screens it is calculated as the following + // bounding box: + // * left: Max(inset left, content view right) + // * right: inset right + // * top: Max(inset top, actionStrip bottom) + // * bottom: inset bottom + Rect landscapeScreenInset = new Rect(inset); + + if (mMapActionStripView.getVisibility() == VISIBLE) { + landscapeScreenInset.right = min(landscapeScreenInset.right, mMapActionStripView.getLeft()); + portraitScreenInset.right = min(portraitScreenInset.right, mMapActionStripView.getLeft()); + } + if (mActionStripView.getVisibility() == VISIBLE) { + landscapeScreenInset.top = max(landscapeScreenInset.top, mActionStripView.getBottom()); + portraitScreenInset.top = max(portraitScreenInset.top, mActionStripView.getBottom()); + } + if (mContentContainer.getVisibility() == View.VISIBLE) { + landscapeScreenInset.left = max(landscapeScreenInset.left, mContentContainer.getRight()); + portraitScreenInset.top = max(portraitScreenInset.top, mContentContainer.getBottom()); + } + int landscapeScreenArea = landscapeScreenInset.height() * landscapeScreenInset.width(); + int portraitScreenArea = portraitScreenInset.height() * portraitScreenInset.width(); + inset.set( + landscapeScreenArea > portraitScreenArea ? landscapeScreenInset : portraitScreenInset); + } + + @Override + public boolean handlesTemplateChangeAnimation() { + return true; + } + + private void update() { + TransitionManager.beginDelayedTransition( + mRootView, + TransitionInflater.from(getTemplateContext()) + .inflateTransition(R.transition.place_list_nav_template_transition)); + + PlaceListNavigationTemplate template = (PlaceListNavigationTemplate) getTemplate(); + + updateActionStrip(template.getActionStrip()); + updateMapActionStrip(template.getMapActionStrip()); + getPanZoomManager().setEnabled(hasPanButton()); + mHeaderView.setContent( + getTemplateContext(), + template.getTitle(), + template.getHeaderAction(), + template.getOnContentRefreshDelegate()); + + ItemList itemList = template.getItemList(); + mContentView.setRowListContent( + getTemplateContext(), + RowListWrapper.wrap(getTemplateContext(), itemList) + .setIsLoading(template.isLoading()) + .setRowFlags(RowWrapper.DEFAULT_UNIFORM_LIST_ROW_FLAGS) + .setRowListConstraints(ROW_LIST_CONSTRAINTS_SIMPLE) + .setIsRefresh(getTemplateWrapper().isRefresh()) + .setIsHalfList(true) + .build()); + + updateVisibility(getPanZoomManager().isInPanMode()); + + getTemplateContext().getSurfaceInfoProvider().invalidateStableArea(); + requestVisibleAreaUpdate(); + } + + private void updateActionStrip(@Nullable ActionStrip actionStrip) { + mActionStripView.setActionStrip( + getTemplateContext(), actionStrip, ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE); + } + + private void updateMapActionStrip(@Nullable ActionStrip actionStrip) { + ActionStripWrapper actionStripWrapper = null; + if (actionStrip != null) { + actionStripWrapper = + getPanZoomManager() + .getMapActionStripWrapper( + /* templateContext= */ getTemplateContext(), /* actionStrip= */ actionStrip); + } + + mMapActionStripView.setActionStrip( + getTemplateContext(), + actionStripWrapper, + ActionsConstraints.ACTIONS_CONSTRAINTS_NAVIGATION_MAP, + /* allowTwoLines= */ false); + } + + @SuppressLint("InflateParams") + @SuppressWarnings({"methodref.receiver.bound.invalid"}) + private PlaceListNavigationTemplatePresenter( + TemplateContext templateContext, TemplateWrapper templateWrapper) { + super(templateContext, templateWrapper, StatusBarState.OVER_SURFACE); + mRootView = + (ViewGroup) + LayoutInflater.from(templateContext) + .inflate(R.layout.list_navigation_template_layout, null); + mContentContainer = mRootView.findViewById(R.id.content_container); + mHeaderView = mRootView.findViewById(R.id.header_view); + mContentView = mRootView.findViewById(R.id.content_view); + mActionStripView = mRootView.findViewById(R.id.action_strip); + mMapActionStripView = mRootView.findViewById(R.id.map_action_strip); + mPanOverlay = mRootView.findViewById(R.id.pan_overlay); + + // We should always show an ItemList. + mContentContainer.setVisibility(View.VISIBLE); + } + + private boolean hasPanButton() { + PlaceListNavigationTemplate template = (PlaceListNavigationTemplate) getTemplate(); + ActionStrip mapActionStrip = template.getMapActionStrip(); + return mapActionStrip != null && mapActionStrip.getFirstActionOfType(Action.TYPE_PAN) != null; + } + + private void dispatchPanModeChange(boolean isInPanMode) { + PlaceListNavigationTemplate template = (PlaceListNavigationTemplate) getTemplate(); + PanModeDelegate panModeDelegate = template.getPanModeDelegate(); + if (panModeDelegate != null) { + getTemplateContext().getAppDispatcher().dispatchPanModeChanged(panModeDelegate, isInPanMode); + } + } + + private void updateVisibility(boolean isInPanMode) { + ThreadUtils.runOnMain( + () -> { + if (isInPanMode) { + mPanOverlay.setVisibility(VISIBLE); + mContentContainer.setVisibility(GONE); + mActionStripView.setVisibility(GONE); + } else { + mPanOverlay.setVisibility(GONE); + mContentContainer.setVisibility(VISIBLE); + mActionStripView.setVisibility(VISIBLE); + } + }); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/RoutePreviewNavigationTemplatePresenter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/RoutePreviewNavigationTemplatePresenter.java new file mode 100644 index 0000000..d7cf9d1 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/RoutePreviewNavigationTemplatePresenter.java @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.presenters.navigation; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static com.android.car.libraries.apphost.distraction.constraints.RowListConstraints.ROW_LIST_CONSTRAINTS_ROUTE_PREVIEW; +import static com.android.car.libraries.apphost.template.view.model.RowListWrapper.LIST_FLAGS_SELECTABLE_FOCUS_SELECT_ROW; +import static com.android.car.libraries.apphost.template.view.model.RowListWrapper.LIST_FLAGS_SELECTABLE_HIGHLIGHT_ROW; +import static com.android.car.libraries.apphost.template.view.model.RowListWrapper.LIST_FLAGS_SELECTABLE_SCROLL_TO_ROW; +import static java.lang.Math.max; +import static java.lang.Math.min; + +import android.annotation.SuppressLint; +import android.graphics.Rect; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.car.app.model.Action; +import androidx.car.app.model.ActionStrip; +import androidx.car.app.model.ItemList; +import androidx.car.app.model.OnClickDelegate; +import androidx.car.app.model.TemplateWrapper; +import androidx.car.app.navigation.model.PanModeDelegate; +import androidx.car.app.navigation.model.RoutePreviewNavigationTemplate; +import com.android.car.libraries.apphost.common.EventManager.EventType; +import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.common.ThreadUtils; +import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints; +import com.android.car.libraries.apphost.template.view.model.ActionStripWrapper; +import com.android.car.libraries.apphost.template.view.model.RowListWrapper; +import com.android.car.libraries.apphost.template.view.model.RowWrapper; +import com.android.car.libraries.apphost.view.AbstractSurfaceTemplatePresenter; +import com.android.car.libraries.apphost.view.TemplatePresenter; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.view.widgets.common.ActionStripView; +import com.android.car.libraries.templates.host.view.widgets.common.CardHeaderView; +import com.android.car.libraries.templates.host.view.widgets.common.ContentView; +import com.android.car.libraries.templates.host.view.widgets.common.PanOverlayView; +import com.google.common.collect.ImmutableList; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** A {@link TemplatePresenter} that shows a {@link RoutePreviewNavigationTemplate}. */ +public class RoutePreviewNavigationTemplatePresenter extends AbstractSurfaceTemplatePresenter { + private final ViewGroup mRootView; + private final ViewGroup mContentContainer; + private final CardHeaderView mHeaderView; + private final ContentView mContentView; + private final ActionStripView mActionStripView; + private final ActionStripView mMapActionStripView; + private final PanOverlayView mPanOverlay; + + /** Creates a {@link RoutePreviewNavigationTemplatePresenter}. */ + public static RoutePreviewNavigationTemplatePresenter create( + TemplateContext templateContext, TemplateWrapper templateWrapper) { + RoutePreviewNavigationTemplatePresenter presenter = + new RoutePreviewNavigationTemplatePresenter(templateContext, templateWrapper); + presenter.update(); + return presenter; + } + + @Override + public View getView() { + return mRootView; + } + + @Override + public void onStart() { + super.onStart(); + + getTemplateContext() + .getEventManager() + .subscribeEvent( + this, + EventType.CONFIGURATION_CHANGED, + () -> { + RoutePreviewNavigationTemplate template = + (RoutePreviewNavigationTemplate) getTemplate(); + updateActionStrip(template.getActionStrip()); + updateMapActionStrip(template.getMapActionStrip()); + }); + } + + @Override + public void onStop() { + getTemplateContext().getEventManager().unsubscribeEvent(this, EventType.CONFIGURATION_CHANGED); + + super.onStop(); + } + + @Override + public void onTemplateChanged() { + update(); + } + + @Override + public boolean isPanAndZoomEnabled() { + return getTemplateContext().getCarHostConfig().isPoiRoutePreviewPanZoomEnabled(); + } + + @Override + public void onPanModeChanged(boolean isInPanMode) { + updateVisibility(isInPanMode); + dispatchPanModeChange(isInPanMode); + } + + @Override + protected View getDefaultFocusedView() { + if (mContentView.getVisibility() == VISIBLE) { + return mContentView; + } + return super.getDefaultFocusedView(); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent keyEvent) { + if (getPanZoomManager().handlePanEventsIfNeeded(keyCode)) { + return true; + } + + // Move between the card view and the action button view on left or right rotary nudge. + if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + // If the focus is in the card view (back button or row list), request focus in the + // action + // strip. + if (moveFocusIfPresent( + ImmutableList.of(mContentContainer), ImmutableList.of(mActionStripView))) { + return true; + } + } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { + // Request focus on the content view so that the first row in the list will take focus. + if (moveFocusIfPresent(ImmutableList.of(mActionStripView), ImmutableList.of(mContentView))) { + return true; + } + } + return super.onKeyUp(keyCode, keyEvent); + } + + public void calculateAdditionalInset(Rect inset) { + // The portrait inset is more favorable to portrait screens and is calculated as the following + // bounding box: + // * left: inset left + // * right: inset right + // * top: Max(inset top, actionStrip bottom, content view bottom) + // * bottom: inset bottom + Rect portraitScreenInset = new Rect(inset); + + // The landscape inset is more favorable to landscape screens it is calculated as the following + // bounding box: + // * left: Max(inset left, content view right) + // * right: inset right + // * top: Max(inset top, actionStrip bottom) + // * bottom: inset bottom + Rect landscapeScreenInset = new Rect(inset); + + if (mMapActionStripView.getVisibility() == VISIBLE) { + landscapeScreenInset.right = min(landscapeScreenInset.right, mMapActionStripView.getLeft()); + portraitScreenInset.right = min(portraitScreenInset.right, mMapActionStripView.getLeft()); + } + if (mActionStripView.getVisibility() == VISIBLE) { + landscapeScreenInset.top = max(landscapeScreenInset.top, mActionStripView.getBottom()); + portraitScreenInset.top = max(portraitScreenInset.top, mActionStripView.getBottom()); + } + if (mContentContainer.getVisibility() == View.VISIBLE) { + landscapeScreenInset.left = max(landscapeScreenInset.left, mContentContainer.getRight()); + portraitScreenInset.top = max(portraitScreenInset.top, mContentContainer.getBottom()); + } + int landscapeScreenArea = landscapeScreenInset.height() * landscapeScreenInset.width(); + int portraitScreenArea = portraitScreenInset.height() * portraitScreenInset.width(); + inset.set( + landscapeScreenArea > portraitScreenArea ? landscapeScreenInset : portraitScreenInset); + } + + private void update() { + RoutePreviewNavigationTemplate template = (RoutePreviewNavigationTemplate) getTemplate(); + + updateActionStrip(template.getActionStrip()); + updateMapActionStrip(template.getMapActionStrip()); + getPanZoomManager().setEnabled(hasPanButton()); + mHeaderView.setContent(getTemplateContext(), template.getTitle(), template.getHeaderAction()); + + ItemList itemList = template.getItemList(); + Action navigateAction = template.getNavigateAction(); + + RowListWrapper.Builder builder = + RowListWrapper.wrap(getTemplateContext(), itemList) + .setIsLoading(template.isLoading()) + .setRowFlags(RowWrapper.DEFAULT_UNIFORM_LIST_ROW_FLAGS) + .setRowListConstraints(ROW_LIST_CONSTRAINTS_ROUTE_PREVIEW) + .setIsRefresh(getTemplateWrapper().isRefresh()) + .setIsHalfList(true) + + // For the route preview list, don't use radio buttons but rather show the + // selection + // by changing the row background. + .setListFlags( + LIST_FLAGS_SELECTABLE_HIGHLIGHT_ROW + | LIST_FLAGS_SELECTABLE_FOCUS_SELECT_ROW + | LIST_FLAGS_SELECTABLE_SCROLL_TO_ROW); + if (!template.isLoading() && navigateAction != null) { + builder.setRowSelectedText(navigateAction.getTitle()); + OnClickDelegate onClickDelegate = navigateAction.getOnClickDelegate(); + if (onClickDelegate != null) { + TemplateContext templateContext = getTemplateContext(); + builder.setOnRepeatedSelectionCallback( + () -> templateContext.getAppDispatcher().dispatchClick(onClickDelegate)); + } + } + mContentView.setRowListContent(getTemplateContext(), builder.build()); + + updateVisibility(getPanZoomManager().isInPanMode()); + + getTemplateContext().getSurfaceInfoProvider().invalidateStableArea(); + requestVisibleAreaUpdate(); + } + + private void updateActionStrip(@Nullable ActionStrip actionStrip) { + mActionStripView.setActionStrip( + getTemplateContext(), actionStrip, ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE); + } + + private void updateMapActionStrip(@Nullable ActionStrip actionStrip) { + ActionStripWrapper actionStripWrapper = null; + if (actionStrip != null) { + actionStripWrapper = + getPanZoomManager() + .getMapActionStripWrapper( + /* templateContext= */ getTemplateContext(), /* actionStrip= */ actionStrip); + } + + mMapActionStripView.setActionStrip( + getTemplateContext(), + actionStripWrapper, + ActionsConstraints.ACTIONS_CONSTRAINTS_NAVIGATION_MAP, + /* allowTwoLines= */ false); + } + + @SuppressLint("InflateParams") + @SuppressWarnings({"methodref.receiver.bound.invalid"}) + private RoutePreviewNavigationTemplatePresenter( + TemplateContext templateContext, TemplateWrapper templateWrapper) { + super(templateContext, templateWrapper, StatusBarState.OVER_SURFACE); + + mRootView = + (ViewGroup) + LayoutInflater.from(templateContext) + .inflate(R.layout.list_navigation_template_layout, null); + mContentContainer = mRootView.findViewById(R.id.content_container); + mHeaderView = mRootView.findViewById(R.id.header_view); + mContentView = mRootView.findViewById(R.id.content_view); + mActionStripView = mRootView.findViewById(R.id.action_strip); + mMapActionStripView = mRootView.findViewById(R.id.map_action_strip); + mPanOverlay = mRootView.findViewById(R.id.pan_overlay); + + mContentContainer.setVisibility(View.VISIBLE); + } + + private boolean hasPanButton() { + RoutePreviewNavigationTemplate template = (RoutePreviewNavigationTemplate) getTemplate(); + ActionStrip mapActionStrip = template.getMapActionStrip(); + return mapActionStrip != null && mapActionStrip.getFirstActionOfType(Action.TYPE_PAN) != null; + } + + private void dispatchPanModeChange(boolean isInPanMode) { + RoutePreviewNavigationTemplate template = (RoutePreviewNavigationTemplate) getTemplate(); + PanModeDelegate panModeDelegate = template.getPanModeDelegate(); + if (panModeDelegate != null) { + getTemplateContext().getAppDispatcher().dispatchPanModeChanged(panModeDelegate, isInPanMode); + } + } + + private void updateVisibility(boolean isInPanMode) { + ThreadUtils.runOnMain( + () -> { + if (isInPanMode) { + mPanOverlay.setVisibility(VISIBLE); + mContentContainer.setVisibility(GONE); + mActionStripView.setVisibility(GONE); + } else { + mPanOverlay.setVisibility(GONE); + mContentContainer.setVisibility(VISIBLE); + mActionStripView.setVisibility(VISIBLE); + } + }); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/anim/travel_estimate_card_hide_animation.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/anim/travel_estimate_card_hide_animation.xml new file mode 100644 index 0000000..c4b2288 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/anim/travel_estimate_card_hide_animation.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<set xmlns:android="http://schemas.android.com/apk/res/android" + android:fillAfter="true"> + <translate + android:fromYDelta="0" + android:toYDelta="100%" + android:duration="@integer/bottom_card_animation_duration_millis" + android:interpolator="@android:interpolator/fast_out_slow_in" + /> +</set> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/anim/travel_estimate_card_show_animation.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/anim/travel_estimate_card_show_animation.xml new file mode 100644 index 0000000..24b34da --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/anim/travel_estimate_card_show_animation.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<set xmlns:android="http://schemas.android.com/apk/res/android" + android:fillAfter="true"> + <translate + android:fromYDelta="100%" + android:toYDelta="0" + android:duration="@integer/bottom_card_animation_duration_millis" + android:interpolator="@android:interpolator/fast_out_slow_in" + /> +</set> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout-w1280dp-h1000dp/list_navigation_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout-w1280dp-h1000dp/list_navigation_template_layout.xml new file mode 100644 index 0000000..ff1e95c --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout-w1280dp-h1000dp/list_navigation_template_layout.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <include + android:id="@id/content_container" + android:layout_width="?templateCardContentContainerDefaultWidth" + android:layout_height="0dp" + android:layout_marginStart="?templateCardContentContainerStartMargin" + android:layout_marginTop="?templateCardContentContainerTopMargin" + app:layout_goneMarginTop="?templateCardContentContainerTopMargin" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/action_strip" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintVertical_bias="0.0" + app:layout_constraintHeight_default="wrap" + app:layout_constraintHeight_min="?templateCardContentContainerMinHeight" + layout="@layout/card_container" /> + + <include + android:id="@+id/pan_overlay" + android:visibility="gone" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + layout="@layout/pan_overlay" /> + + <!-- The action strip with global actions for the template. --> + <include + android:id="@+id/action_strip" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + layout="@layout/action_strip_view_floating" /> + + <include + android:id="@+id/map_action_strip" + layout="@layout/map_action_strip_view_floating"/> +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout-w1280dp-h1000dp/navigation_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout-w1280dp-h1000dp/navigation_template_layout.xml new file mode 100644 index 0000000..147e2c8 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout-w1280dp-h1000dp/navigation_template_layout.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <!-- This is the card that contains the routing information, arrival view, + etc. --> + <include + android:id="@id/content_container" + android:layout_width="?templateRoutingStepsCardContentContainerMinWidth" + android:layout_height="0dp" + android:layout_marginStart="?templateCardContentContainerStartMargin" + android:layout_marginTop="?templateCardContentContainerTopMargin" + android:layout_marginBottom="?templateCardContentContainerBottomMargin" + app:layout_goneMarginTop="?templateCardContentContainerTopMargin" + app:layout_constraintTop_toBottomOf="@id/action_strip" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintVertical_bias="0.0" + app:layout_constraintHeight_default="wrap" + app:layout_constraintHeight_min="?templateRoutingStepsCardContentContainerMinHeight" + layout="@layout/steps_card_container"/> + + <!-- The travel estimate (aka "ETA") card at the bottom left of the + screen. --> + <include + layout="@layout/travel_estimate_card_container" + android:visibility="gone" /> + + <include + android:id="@+id/pan_overlay" + android:visibility="gone" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + layout="@layout/pan_overlay" /> + + <!-- The action strip with global actions for the template. --> + <include + android:id="@+id/action_strip" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + layout="@layout/action_strip_view_floating"/> + + <include + android:id="@+id/map_action_strip" + layout="@layout/map_action_strip_view_floating"/> +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/list_navigation_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/list_navigation_template_layout.xml new file mode 100644 index 0000000..cba4ed4 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/list_navigation_template_layout.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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" + android:layout_width="match_parent" + android:layout_height="match_parent" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <include + android:id="@id/content_container" + layout="@layout/card_container" /> + + <include + android:id="@+id/pan_overlay" + android:visibility="gone" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + layout="@layout/pan_overlay" /> + + <!-- The action strip with global actions for the template. --> + <include + android:id="@+id/action_strip" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + layout="@layout/action_strip_view_floating" /> + + <include + android:id="@+id/map_action_strip" + layout="@layout/map_action_strip_view_floating"/> +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/navigation_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/navigation_template_layout.xml new file mode 100644 index 0000000..c2dfcef --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/navigation_template_layout.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <!-- This is the card that contains the routing information, arrival view, + etc. --> + <include + android:id="@id/content_container" + layout="@layout/steps_card_container"/> + + <!-- The travel estimate (aka "ETA") card at the bottom left of the + screen. --> + <include + layout="@layout/travel_estimate_card_container" + android:visibility="gone" /> + + <include + android:id="@+id/pan_overlay" + android:visibility="gone" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + layout="@layout/pan_overlay" /> + + <!-- The action strip with global actions for the template. --> + <include + android:id="@+id/action_strip" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + layout="@layout/action_strip_view_floating"/> + + <include + android:id="@+id/map_action_strip" + layout="@layout/map_action_strip_view_floating"/> +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/steps_card_container.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/steps_card_container.xml new file mode 100644 index 0000000..1eb797f --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/steps_card_container.xml @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!-- +The card has a minimum and maximum heights specific to the routing screen. +The card is anchored to the top left of the screen which should be consistent +with the rest of the card-style templates +--> +<com.android.car.libraries.templates.host.view.widgets.common.BleedingCardView + 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" + style="?templateCardRoutingContentContainerStyle" + android:focusable="false" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintVertical_bias="0.0" + app:layout_constraintHeight_default="wrap" + app:layout_constraintHeight_min="?templateRoutingStepsCardContentContainerMinHeight" + android:layout_marginStart="?templateCardContentContainerStartMargin" + android:layout_marginTop="?templateCardContentContainerTopMargin" + android:layout_marginBottom="?templateCardContentContainerBottomMargin" + android:layout_width="?templateRoutingStepsCardContentContainerMinWidth" + android:layout_height="0dp" + tools:ignore="MissingClass"> + + <!-- A view that shows a message. --> + <include + android:id="@+id/message_view" + android:visibility="gone" + layout="@layout/message_view" /> + + <!-- The container view for progress indicator. --> + <include + android:id="@+id/progress_view" + android:visibility="gone" + layout="@layout/progress_view" /> + + <!-- A container view for the driving instruction, such as the turn icon and + junction image.--> + <LinearLayout + android:id="@+id/steps_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <!-- A detailed view showing the current step. --> + <include + android:id="@+id/detailed_step_view" + layout="@layout/detailed_step_view" /> + + <!-- A divider between the current step and the next step. --> + <View + android:id="@+id/divider" + android:layout_width="match_parent" + android:layout_height="?templateDividerThickness" + android:background="?templateRoutingDividerColor"/> + + <!-- A compact view showing the next step. --> + <include + android:id="@+id/compact_step_view" + layout="@layout/compact_step_view" /> + + <!-- The optional junction image. --> + <FrameLayout + android:id="@+id/junction_image_container" + android:background="?templateRoutingJunctionImageBackgroundColor" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <ImageView + android:id="@+id/junction_image" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:clickable="false" + android:focusable="false" + android:scaleType="centerInside" + android:adjustViewBounds="true" + tools:ignore="ContentDescription" /> + </FrameLayout> + </LinearLayout> +</com.android.car.libraries.templates.host.view.widgets.common.BleedingCardView> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/travel_estimate_card_container.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/travel_estimate_card_container.xml new file mode 100644 index 0000000..498304d --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/travel_estimate_card_container.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<com.android.car.libraries.templates.host.view.widgets.common.BleedingCardView + android:id="@+id/travel_estimate_card_container" + xmlns:app="http://schemas.android.com/apk/res-auto" + style="?templateCardRoutingContentContainerStyle" + android:focusable="false" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + android:layout_width="?templateRoutingStepsCardContentContainerMinWidth" + android:layout_height="wrap_content" + android:layout_marginStart="?templateCardContentContainerStartMargin" + android:layout_marginTop="?templateCardContentContainerTopMargin" + android:paddingHorizontal="?templateNavCardPaddingHorizontal" + android:paddingVertical="?templateNavCardSmallPaddingVertical" + xmlns:android="http://schemas.android.com/apk/res/android"> + + <!-- The travel estimate (aka ETA) card. --> + <include + layout="@layout/travel_estimate_view" + android:id="@+id/travel_estimate_view" + android:layout_gravity="center_vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> +</com.android.car.libraries.templates.host.view.widgets.common.BleedingCardView> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/transition/place_list_nav_template_transition.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/transition/place_list_nav_template_transition.xml new file mode 100644 index 0000000..bedd592 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/transition/place_list_nav_template_transition.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<transitionSet xmlns:android="http://schemas.android.com/apk/res/android" + android:transitionOrdering="together"> + <fade + android:fadingMode="fade_in_out" + android:duration="?templateUpdateAnimationDurationMilliseconds"/> + <changeBounds + android:duration="?templateUpdateAnimationDurationMilliseconds" + android:interpolator="@interpolator/fast_out_slow_in"/> +</transitionSet> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/transition/routing_card_transition.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/transition/routing_card_transition.xml new file mode 100644 index 0000000..9a18a4e --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/transition/routing_card_transition.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<transitionSet xmlns:android="http://schemas.android.com/apk/res/android" + android:transitionOrdering="together"> + <targets> + <target android:targetId="@id/content_container" /> + <target android:targetId="@id/travel_estimate_card_container" /> + <target android:targetId="@id/pan_overlay" /> + </targets> + <fade + android:fadingMode="fade_in_out" + android:duration="@integer/routing_card_animation_duration_millis"> + <targets> + <target android:excludeId="@id/detailed_step_view" /> + <target android:excludeId="@id/travel_estimate_card_container" /> + </targets> + </fade> + <changeBounds + android:duration="@integer/routing_card_animation_duration_millis" + android:interpolator="@interpolator/fast_out_slow_in"/> +</transitionSet> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/values/integers.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/values/integers.xml new file mode 100644 index 0000000..6ac3813 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/values/integers.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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> + <integer name="routing_card_animation_duration_millis">500</integer> + <integer name="bottom_card_animation_duration_millis">500</integer> +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/anim/default_progress_indeterminate_material.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/anim/default_progress_indeterminate_material.xml new file mode 100644 index 0000000..d4d7aea --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/anim/default_progress_indeterminate_material.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!-- +Copied from the Android SDK's internal resources +Source: //third_party/java/android/android_sdk_linux/platforms/stable/data/res/anim/progress_indeterminate_material.xml +--> +<set xmlns:android="http://schemas.android.com/apk/res/android"> + <objectAnimator + android:duration="1333" + android:interpolator="@interpolator/default_trim_start_interpolator" + android:propertyName="trimPathStart" + android:repeatCount="-1" + android:valueFrom="0" + android:valueTo="0.75" + android:valueType="floatType" /> + <objectAnimator + android:duration="1333" + android:interpolator="@interpolator/default_trim_end_interpolator" + android:propertyName="trimPathEnd" + android:repeatCount="-1" + android:valueFrom="0" + android:valueTo="0.75" + android:valueType="floatType" /> + <objectAnimator + android:duration="1333" + android:interpolator="@android:anim/linear_interpolator" + android:propertyName="trimPathOffset" + android:repeatCount="-1" + android:valueFrom="0" + android:valueTo="0.25" + android:valueType="floatType" /> +</set> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/anim/default_progress_indeterminate_rotation_material.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/anim/default_progress_indeterminate_rotation_material.xml new file mode 100644 index 0000000..d267c85 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/anim/default_progress_indeterminate_rotation_material.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!-- +Copied from the Android SDK's internal resources +Source: //third_party/java/android/android_sdk_linux/platforms/stable/data/res/anim/progress_indeterminate_rotation_material.xml +--> +<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" + android:duration="6665" + android:interpolator="@android:anim/linear_interpolator" + android:propertyName="rotation" + android:repeatCount="-1" + android:valueFrom="0" + android:valueTo="720" + android:valueType="floatType" /> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color-dpad/template_ripple_color_selector.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color-dpad/template_ripple_color_selector.xml new file mode 100644 index 0000000..f988a33 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color-dpad/template_ripple_color_selector.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!-- Due to b/120995067 in order to keep normal ripple effect in touch, the color selector must show +for all states. In rotary and touchpad, per go/aa-focus-design, don't draw ripple when not pressed. +Doing so also avoids "ghost" effect when rapidly moving focus across Views. --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="?templateRippleSelectorColor" + android:state_pressed="true"/> + <item android:color="@android:color/transparent"/> +</selector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color-wheel/template_ripple_color_selector.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color-wheel/template_ripple_color_selector.xml new file mode 100644 index 0000000..f988a33 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color-wheel/template_ripple_color_selector.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!-- Due to b/120995067 in order to keep normal ripple effect in touch, the color selector must show +for all states. In rotary and touchpad, per go/aa-focus-design, don't draw ripple when not pressed. +Doing so also avoids "ghost" effect when rapidly moving focus across Views. --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="?templateRippleSelectorColor" + android:state_pressed="true"/> + <item android:color="@android:color/transparent"/> +</selector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color/template_focus_ring_color_selector.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color/template_focus_ring_color_selector.xml new file mode 100644 index 0000000..ae18391 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color/template_focus_ring_color_selector.xml @@ -0,0 +1,9 @@ +<selector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <!-- Inactive state --> + <item app:templateFocusStateInactive="true" android:color="?templateFocusRingNoAccentColor"/> + + <!-- Default --> + <item android:color="?templateFocusRingColor"/> +</selector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color/template_ripple_color_selector.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color/template_ripple_color_selector.xml new file mode 100644 index 0000000..6ad27df --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color/template_ripple_color_selector.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!-- Due to b/120995067 in order to keep normal ripple effect in touch, the color selector must show +for all states. In rotary and touchpad, per go/aa-focus-design, don't draw ripple when not pressed. +Doing so also avoids "ghost" effect when rapidly moving focus across Views. --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="?templateRippleSelectorColor"/> +</selector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable-night/action_strip_fab_view_background.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable-night/action_strip_fab_view_background.xml new file mode 100644 index 0000000..2c5f9aa --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable-night/action_strip_fab_view_background.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- The background fill --> + <item> + <shape android:shape="rectangle"> + <solid android:color="?templateActionStripFabBackgroundColorDark"/> + <corners + android:radius="?templateButtonCornerRadius"/> + </shape> + </item> + + <!-- Masked ripple layer --> + <item android:drawable="@drawable/action_strip_button_ripple"/> +</layer-list> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_button_focus_ring.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_button_focus_ring.xml new file mode 100644 index 0000000..3769aab --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_button_focus_ring.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_hovered="true" android:state_window_focused="true"> + <shape android:shape="rectangle"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_hovered" + android:color="?templateFocusAccentColor"/> + <corners android:radius="?templateButtonCornerRadius"/> + </shape> + </item> + <item android:state_focused="true" android:state_window_focused="true"> + <shape android:shape="rectangle"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_focused" + android:color="?templateFocusAccentColor"/> + <corners android:radius="?templateButtonCornerRadius"/> + </shape> + </item> +</selector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_button_ripple.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_button_ripple.xml new file mode 100644 index 0000000..ba659c5 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_button_ripple.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/template_ripple_color_selector"> + <!-- Ripple layer masked so that it is only drawn within the bounds of the view --> + <item + android:id="@android:id/mask" + android:gravity="center"> + <shape android:shape="rectangle"> + <solid android:color="@color/template_ripple_color_selector"/> + <corners android:radius="?templateButtonCornerRadius"/> + </shape> + </item> +</ripple> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_button_view_background.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_button_view_background.xml new file mode 100644 index 0000000..01508d9 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_button_view_background.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- The background fill --> + <item> + <shape android:shape="rectangle"> + <solid android:color="?templateActionStripButtonBackgroundColor"/> + <corners + android:radius="?templateButtonCornerRadius"/> + <stroke + android:width="?templateActionButtonSecondaryBorderWidth" + android:color="?templateActionButtonSecondaryBorderColor" /> + </shape> + </item> + + <!-- Masked ripple layer --> + <item android:drawable="@drawable/action_strip_button_ripple"/> +</layer-list> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_button_view_focus_ring.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_button_view_focus_ring.xml new file mode 100644 index 0000000..e97b6ab --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_button_view_focus_ring.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_hovered="true" android:state_window_focused="true"> + <shape android:shape="rectangle"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_hovered" + android:color="?templateFocusAccentColor" /> + <corners android:radius="?templateButtonCornerRadius" /> + </shape> + </item> + <item android:state_focused="true" android:state_window_focused="true"> + <shape android:shape="rectangle"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_focused" + android:color="?templateFocusAccentColor" /> + <corners android:radius="?templateButtonCornerRadius" /> + </shape> + </item> +</selector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_fab_view_background.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_fab_view_background.xml new file mode 100644 index 0000000..37591ff --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_fab_view_background.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- The background fill --> + <item> + <shape android:shape="rectangle"> + <solid android:color="?templateActionStripFabBackgroundColorLight"/> + <corners + android:radius="?templateButtonCornerRadius"/> + </shape> + </item> + + <!-- Masked ripple layer --> + <item android:drawable="@drawable/action_strip_button_ripple"/> +</layer-list> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/back_button_focus_ring.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/back_button_focus_ring.xml new file mode 100644 index 0000000..4f5868f --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/back_button_focus_ring.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<inset xmlns:android="http://schemas.android.com/apk/res/android" + android:inset="@dimen/template_back_focus_ring_inset"> + <selector> + <item android:state_hovered="true" android:state_window_focused="true"> + <shape android:shape="oval"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_hovered" + android:color="?templateFocusAccentColor"/> + </shape> + </item> + <item android:state_focused="true" android:state_window_focused="true"> + <shape android:shape="oval"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_focused" + android:color="?templateFocusAccentColor"/> + </shape> + </item> + </selector> +</inset> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/default_progress_spinner.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/default_progress_spinner.xml new file mode 100644 index 0000000..4f0bf0f --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/default_progress_spinner.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + android:drawable="@drawable/default_progress_spinner_medium_thin"> + <target + android:name="progressBar" + android:animation="@anim/default_progress_indeterminate_material" /> + <target + android:name="root" + android:animation="@anim/default_progress_indeterminate_rotation_material" /> +</animated-vector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/default_progress_spinner_medium_thin.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/default_progress_spinner_medium_thin.xml new file mode 100644 index 0000000..c5efbed --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/default_progress_spinner_medium_thin.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportHeight="48" + android:viewportWidth="48"> + <group + android:name="root" + android:translateX="24.0" + android:translateY="24.0"> + <path + android:name="progressBar" + android:fillColor="#00000000" + android:pathData="M0, 0 m 0, -19 a 19,19 0 1,1 0,38 a 19,19 0 1,1 0,-38" + android:strokeColor="?templateLoadingSpinnerColor" + android:strokeLineCap="square" + android:strokeLineJoin="miter" + android:strokeWidth="3" + android:trimPathEnd="0" + android:trimPathOffset="0" + android:trimPathStart="0" /> + </group> +</vector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/no_content_view_focus_ring.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/no_content_view_focus_ring.xml new file mode 100644 index 0000000..d488ba4 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/no_content_view_focus_ring.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<inset xmlns:android="http://schemas.android.com/apk/res/android" + android:inset="@dimen/template_no_content_view_focus_ring_padding"> + <selector> + <item android:state_hovered="true" android:state_window_focused="true"> + <shape android:shape="rectangle"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_hovered" + android:color="?templateFocusNoContentAccentColor"/> + <corners android:radius="?templateNoContentFocusCornerRadius"/> + </shape> + </item> + <item android:state_focused="true" android:state_window_focused="true"> + <shape android:shape="rectangle"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_focused" + android:color="?templateFocusNoContentAccentColor"/> + <corners android:radius="?templateNoContentFocusCornerRadius"/> + </shape> + </item> + </selector> +</inset> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/pin_sign_in_view_background.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/pin_sign_in_view_background.xml new file mode 100644 index 0000000..42be196 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/pin_sign_in_view_background.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="?templateSignInPinBackgroundColor" /> + <corners android:radius="?templateSignInPinCornerRadius" /> +</shape>
\ No newline at end of file diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_bottom.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_bottom.xml new file mode 100644 index 0000000..a24754e --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_bottom.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?templateRippleColor"> + + <!-- Background fill --> + <item> + <selector> + <item android:state_selected="true"> + <shape android:shape="rectangle"> + <solid android:color="?templateRowSelectedBackgroundColor" /> + <corners + android:bottomRightRadius="@dimen/template_row_corner_radius" + android:bottomLeftRadius="@dimen/template_row_corner_radius"/> + </shape> + </item> + <item android:state_selected="false"> + <shape android:shape="rectangle"> + <solid android:color="?templateRowBackgroundColor" /> + <corners + android:bottomRightRadius="@dimen/template_row_corner_radius" + android:bottomLeftRadius="@dimen/template_row_corner_radius"/> + </shape> + </item> + </selector> + </item> + + <!-- Ripple layer masked so that it is only drawn within the bounds of the view --> + <item android:id="@android:id/mask" + android:gravity="center"> + <shape android:shape="rectangle"> + <solid android:color="@color/template_ripple_color_selector"/> + <corners + android:bottomRightRadius="@dimen/template_row_corner_radius" + android:bottomLeftRadius="@dimen/template_row_corner_radius"/> + </shape> + </item> + + <!-- Child layer for drawing foreground ring in hovered and focused states. --> + <item> + <selector> + <item android:state_hovered="true" android:state_window_focused="true"> + <shape android:shape="rectangle"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_hovered" + android:color="?templateFocusAccentColor"/> + <corners + android:bottomLeftRadius="@dimen/template_row_corner_radius" + android:bottomRightRadius="@dimen/template_row_corner_radius"/> + </shape> + </item> + <item android:state_focused="true" android:state_window_focused="true"> + <shape android:shape="rectangle"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_focused" + android:color="?templateFocusAccentColor"/> + <corners + android:bottomLeftRadius="@dimen/template_row_corner_radius" + android:bottomRightRadius="@dimen/template_row_corner_radius"/> + </shape> + </item> + </selector> + </item> +</ripple> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_middle.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_middle.xml new file mode 100644 index 0000000..e52e937 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_middle.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?templateRippleColor"> + + <!-- Background fill --> + <item> + <selector> + <item android:state_selected="true"> + <shape android:shape="rectangle"> + <solid android:color="?templateRowSelectedBackgroundColor" /> + </shape> + </item> + <item android:state_selected="false"> + <shape android:shape="rectangle"> + <solid android:color="?templateRowBackgroundColor" /> + </shape> + </item> + </selector> + </item> + + <!-- Ripple layer masked so that it is only drawn within the bounds of the view --> + <item android:id="@android:id/mask" + android:gravity="center"> + <shape android:shape="rectangle"> + <solid android:color="@color/template_ripple_color_selector"/> + </shape> + </item> + + <!-- Child layer for drawing foreground ring in hovered and focused states. --> + <item> + <selector> + <item android:state_hovered="true" android:state_window_focused="true"> + <shape android:shape="rectangle"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_hovered" + android:color="?templateFocusAccentColor"/> + </shape> + </item> + <item android:state_focused="true" android:state_window_focused="true"> + <shape android:shape="rectangle"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_focused" + android:color="?templateFocusAccentColor"/> + </shape> + </item> + </selector> + </item> +</ripple> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_top.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_top.xml new file mode 100644 index 0000000..50b092b --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_top.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?templateRippleColor"> + + <!-- Background fill --> + <item> + <selector> + <item android:state_selected="true"> + <shape android:shape="rectangle"> + <solid android:color="?templateRowSelectedBackgroundColor" /> + <corners + android:topRightRadius="@dimen/template_row_corner_radius" + android:topLeftRadius="@dimen/template_row_corner_radius"/> + </shape> + </item> + <item android:state_selected="false"> + <shape android:shape="rectangle"> + <solid android:color="?templateRowBackgroundColor" /> + <corners + android:topRightRadius="@dimen/template_row_corner_radius" + android:topLeftRadius="@dimen/template_row_corner_radius"/> + </shape> + </item> + </selector> + </item> + + <!-- Ripple layer masked so that it is only drawn within the bounds of the view --> + <item android:id="@android:id/mask" + android:gravity="center"> + <shape android:shape="rectangle"> + <solid android:color="@color/template_ripple_color_selector"/> + <corners + android:topRightRadius="@dimen/template_row_corner_radius" + android:topLeftRadius="@dimen/template_row_corner_radius"/> + </shape> + </item> + + <!-- Child layer for drawing foreground ring in hovered and focused states. --> + <item> + <selector> + <item android:state_hovered="true" android:state_window_focused="true"> + <shape android:shape="rectangle"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_hovered" + android:color="?templateFocusAccentColor"/> + <corners + android:topLeftRadius="@dimen/template_row_corner_radius" + android:topRightRadius="@dimen/template_row_corner_radius"/> + </shape> + </item> + <item android:state_focused="true" android:state_window_focused="true"> + <shape android:shape="rectangle"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_focused" + android:color="?templateFocusAccentColor"/> + <corners + android:topLeftRadius="@@dimen/template_row_corner_radius" + android:topRightRadius="@dimen/template_row_corner_radius"/> + </shape> + </item> + </selector> + </item> +</ripple> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_top_bottom.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_top_bottom.xml new file mode 100644 index 0000000..d7e2b4a --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_top_bottom.xml @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?templateRippleColor"> + + <!-- Background fill --> + <item> + <selector> + <item android:state_selected="true"> + <shape android:shape="rectangle"> + <solid android:color="?templateRowSelectedBackgroundColor"/> + <corners + android:radius="@dimen/template_row_corner_radius"/> + </shape> + </item> + <item android:state_selected="false"> + <shape android:shape="rectangle"> + <solid android:color="?templateRowBackgroundColor" /> + <corners + android:radius="@dimen/template_row_corner_radius"/> + </shape> + </item> + </selector> + </item> + + <!-- Ripple layer masked so that it is only drawn within the bounds of the view --> + <item android:id="@android:id/mask" + android:gravity="center"> + <shape android:shape="rectangle"> + <solid android:color="@color/template_ripple_color_selector"/> + <corners + android:radius="@dimen/template_row_corner_radius"/> + </shape> + </item> + + <!-- Child layer for drawing foreground ring in hovered and focused states. --> + <item> + <selector> + <item android:state_hovered="true" android:state_window_focused="true"> + <shape android:shape="rectangle"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_hovered" + android:color="?templateFocusAccentColor"/> + <corners + android:radius="@dimen/template_row_corner_radius"/> + </shape> + </item> + <item android:state_focused="true" android:state_window_focused="true"> + <shape android:shape="rectangle"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_focused" + android:color="?templateFocusAccentColor"/> + <corners + android:radius="@dimen/template_row_corner_radius"/> + </shape> + </item> + </selector> + </item> +</ripple> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_simple.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_simple.xml new file mode 100644 index 0000000..58dad27 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_simple.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?templateRippleColor"> + + <!-- Background fill --> + <item> + <selector> + <item android:state_selected="true"> + <shape + android:shape="rectangle"> + <solid android:color="?templateRowSelectedBackgroundColor" /> + </shape> + </item> + <item android:state_selected="false"> + <shape android:shape="rectangle"> + <solid android:color="@android:color/transparent" /> + </shape> + </item> + </selector> + </item> + + <!-- Ripple layer masked so that it is only drawn within the bounds of the view --> + <item android:id="@android:id/mask" + android:gravity="center"> + <shape android:shape="rectangle"> + <solid android:color="@color/template_ripple_color_selector"/> + </shape> + </item> + + <!-- Child layer for drawing foreground ring in hovered and focused states. --> + <item> + <selector> + <item android:state_hovered="true" android:state_window_focused="true"> + <shape android:shape="rectangle"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_hovered" + android:color="?templateFocusAccentColor"/> + </shape> + </item> + <item android:state_focused="true" android:state_window_focused="true"> + <shape android:shape="rectangle"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_focused" + android:color="?templateFocusAccentColor"/> + </shape> + </item> + </selector> + </item> +</ripple> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_image_placeholder.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_image_placeholder.xml new file mode 100644 index 0000000..5142deb --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_image_placeholder.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="oval"> + <solid android:color="?templateRowImagePlaceholderColor" /> +</shape> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/search_bar_icon.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/search_bar_icon.xml new file mode 100644 index 0000000..ade70ba --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/search_bar_icon.xml @@ -0,0 +1,5 @@ +<vector android:height="36dp" android:tint="#FFFFFF" + android:viewportHeight="24.0" android:viewportWidth="24.0" + android:width="36dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@color/template_edit_text_color_selector" android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/> +</vector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/template_grid_item_background.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/template_grid_item_background.xml new file mode 100644 index 0000000..1f80d18 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/template_grid_item_background.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?templateRippleColor"> + + <!-- Ripple layer masked so that it is only drawn within the bounds of the view --> + <item + android:id="@android:id/mask" + android:gravity="center"> + <shape android:shape="rectangle"> + <solid android:color="@color/template_ripple_color_selector"/> + <corners + android:radius="@dimen/template_grid_item_corner_radius"/> + </shape> + </item> + + <!-- Child layer for drawing foreground ring in hovered and focused states. --> + <item> + <selector> + <item android:state_hovered="true" android:state_window_focused="true"> + <shape android:shape="rectangle"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_hovered" + android:color="?templateFocusAccentColor"/> + <corners + android:radius="@dimen/template_grid_item_corner_radius"/> + </shape> + </item> + <item android:state_focused="true" android:state_window_focused="true"> + <shape android:shape="rectangle"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_focused" + android:color="?templateFocusAccentColor"/> + <corners + android:radius="@dimen/template_grid_item_corner_radius"/> + </shape> + </item> + </selector> + </item> + +</ripple> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/template_header_button_background.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/template_header_button_background.xml new file mode 100644 index 0000000..00a5abb --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/template_header_button_background.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<inset xmlns:android="http://schemas.android.com/apk/res/android" + android:inset="@dimen/template_header_button_focus_inset" + android:drawable="@drawable/template_oval_focus_background"/> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/template_oval_focus_background.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/template_oval_focus_background.xml new file mode 100644 index 0000000..f8a89ae --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/template_oval_focus_background.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?templateRippleColor"> + + <!-- Ripple layer masked inset the thickness of the ring so the ripple layer + is only drawn within the bounds of the ring --> + <item + android:id="@android:id/mask" + android:gravity="center"> + <inset android:inset="@dimen/template_focus_oval_ripple_inset"> + <shape android:shape="oval"> + <solid android:color="@color/template_ripple_color_selector"/> + </shape> + </inset> + </item> + + <!-- Child layer for drawing foreground ring in hovered and focused states. --> + <item> + <selector> + <item android:state_hovered="true" android:state_window_focused="true"> + <shape android:shape="oval"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_hovered" + android:color="?templateFocusAccentColor"/> + </shape> + </item> + <item android:state_focused="true" android:state_window_focused="true"> + <shape android:shape="oval"> + <stroke + android:width="@dimen/template_focus_ring_stroke_width_focused" + android:color="?templateFocusAccentColor"/> + </shape> + </item> + </selector> + </item> +</ripple> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/interpolator/default_trim_end_interpolator.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/interpolator/default_trim_end_interpolator.xml new file mode 100644 index 0000000..de39d96 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/interpolator/default_trim_end_interpolator.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android" + android:pathData="C0.2,0 0.1,1 0.5, 1 L 1,1" /> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/interpolator/default_trim_start_interpolator.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/interpolator/default_trim_start_interpolator.xml new file mode 100644 index 0000000..34eba93 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/interpolator/default_trim_start_interpolator.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android" + android:pathData="L0.5,0 C 0.7,0 0.6,1 1, 1" /> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/layout/template_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/layout/template_view.xml new file mode 100644 index 0000000..67bc95b --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/layout/template_view.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!--The template view is marked as focusable in case nothing in the template presenter is focusable. +It will only take focus if no view under its hierarchy is focusable. +This is needed for the touchpad mode, which requires at least one focusable view in the screen. --> +<com.android.car.libraries.templates.host.view.TemplateView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/template_view" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <com.android.car.ui.FocusParkingView + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + + <com.android.car.libraries.apphost.view.SurfaceViewContainer + android:id="@+id/surface_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="gone"/> + + <FrameLayout + android:id="@+id/template_container" + android:layout_width="match_parent" + android:layout_height="match_parent"/> + + <TextView + android:id="@+id/debug_overlay" + android:layout_gravity="bottom|end" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="?templateDebugMessageBackgroundColor" + android:layout_margin="10dp" + android:padding="5dp" + android:gravity="end" + style="?templateMessageDebugTextStyle" + android:visibility="gone"/> + +</com.android.car.libraries.templates.host.view.TemplateView> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-night/colors.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-night/colors.xml new file mode 100644 index 0000000..799ad34 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-night/colors.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!-- Color definitions for the UI in the templates. + + !!! IMPORTANT !!! + Do not refer to these colors directly from views. Colors must be referred + to through theme attributes (in attrs.xml). + + !!! IMPORTANT !!! + Do not define colors with a RGB value in here (e.g. #FF9B4DF8). These + colors should all refer to colors defined in @color/default. + + If you need a color that's not there already, make a change to add it and + get that approved. --> +<resources> + <!-- Status bar. --> + <color name="template_status_bar_end_color">@color/default_black</color> + + <!-- Night mode color scheme for map markers. --> + <color name="template_marker_default_background_color">@color/default_white</color> + <color name="template_marker_map_default_content_color">@color/default_black</color> + <color name="template_marker_list_default_content_color">@color/default_white</color> + <color name="template_marker_custom_background_content_color">@color/default_black</color> + <color name="template_marker_default_border_color">@color/default_black</color> + <color name="template_marker_custom_border_color">@color/default_black</color> + <color name="template_anchor_default_background_color">@color/default_white</color> + <color name="template_anchor_border_color">@color/default_black</color> + <color name="template_anchor_dot_color">@color/default_gradient_black_64</color> +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w600dp/integers.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w600dp/integers.xml new file mode 100644 index 0000000..65eecaa --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w600dp/integers.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!-- TODO(b/171342295): Update the dp threshold for deciding number of grid items.--> +<resources> + <integer name="template_grid_items_per_row">3</integer> +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w930dp-h520dp/dimens.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w930dp-h520dp/dimens.xml new file mode 100644 index 0000000..99cfd75 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w930dp-h520dp/dimens.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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> + <!-- Markers. --> + <dimen name="template_marker_icon_size">48dp</dimen> + <dimen name="template_marker_image_size">54dp</dimen> +</resources>
\ No newline at end of file diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w930dp-h520dp/styles.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w930dp-h520dp/styles.xml new file mode 100644 index 0000000..6fef3f0 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w930dp-h520dp/styles.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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> + <!-- The appearance of the markers in the map view. --> + <style name="TextAppearance.Marker" parent="TextAppearance.Template.Body1"> + <item name="android:fontFamily">sans-serif-medium</item> + </style> +</resources>
\ No newline at end of file diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w930dp/integers.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w930dp/integers.xml new file mode 100644 index 0000000..a59a90d --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w930dp/integers.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!-- TODO(b/171342295): Update the dp threshold for deciding number of grid items.--> +<resources> + <integer name="template_grid_items_per_row">4</integer> +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/attrs.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/attrs.xml new file mode 100644 index 0000000..958b65b --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/attrs.xml @@ -0,0 +1,624 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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> + <!-- The theme attributes for the template UI. + Template layouts and their widgets should not hardcode any styles for the most part but instead + use references to these theme attributes, thus making the look and feel of the template UI + completely defined by this theme. --> + <declare-styleable name="TemplateTheme"> + <!-- Map markers. --> + <!-- The appearance of markers in the map view. --> + <attr name="templateMapMarkerAppearance" format="reference"/> + + <!-- The appearance of markers in the list view. --> + <attr name="templateListMarkerAppearance" format="reference"/> + + <!-- Content containers --> + <!-- Plain content container attributes. A plain container displays the + content on a flat surface with no rounded corners, shadows, etc. --> + <attr name="templatePlainContentContainerStyle" format="reference"/> + <attr name="templatePlainContentContainerWidth" format="dimension"/> + <attr name="templatePlainContentLayoutGravity" format="integer"/> + <attr name="templatePlainContentGravity" format="integer"/> + <attr name="templatePlainContentHorizontalPadding" format="dimension"/> + <attr name="templatePlainContentBackgroundColor" format="color"/> + + <!-- Card content container attributes. --> + <attr name="templateCardContentContainerStyle" format="reference"/> + <attr name="templateCardRoutingContentContainerStyle" format="reference"/> + <attr name="templateCardContentContainerDefaultWidth" format="dimension"/> + <attr name="templateCardContentContainerStartMargin" format="dimension"/> + <attr name="templateCardContentContainerTopMargin" format="dimension"/> + <attr name="templateCardContentContainerBottomMargin" format="dimension"/> + <attr name="templateCardContentContainerMinHeight" format="dimension"/> + + <!-- Headers --> + <!-- The height of the header, which contains a title and other elements + such as the app icon or a back button. --> + <attr name="templateHeaderHeight" format="dimension"/> + + <!-- The text style to be used in the header. --> + <attr name="templateHeaderTextStyle" format="reference"/> + + <!-- The size of the icon of a button in the header (aspect ratio 1:1). --> + <attr name="templateHeaderButtonIconSize" format="dimension"/> + + <!-- The color of the tint for header buttons (e.g. the back button). --> + <attr name="templateHeaderButtonIconTint" format="color"/> + + <!-- The size of the button container in the header (aspect ratio 1:1). --> + <attr name="templateHeaderButtonContainerSize" format="dimension"/> + + <!-- The start margin of the button container in the header. --> + <attr name="templateHeaderButtonStartSpacing" format="dimension"/> + + <!-- Height of the buttons. Both action buttons and FAB. --> + <attr name="templateButtonHeight" format="dimension"/> + + <!-- The corner radius used for non-button components. --> + <attr name="templateCornerRadius" format="dimension"/> + + <!-- The corner radius use for the buttons. --> + <attr name="templateButtonCornerRadius" format="dimension"/> + + <!-- The spacing around the header title. --> + <attr name="templateHeaderTextVerticalSpacing" format="dimension"/> + <attr name="templateHeaderTextStartSpacing" format="dimension"/> + <attr name="templateHeaderTextEndSpacing" format="dimension"/> + <attr name="templateHeaderTextNoIconStartSpacing" format="dimension"/> + + <!-- The background of a header button. Used to implement focus selection + state, ripple effects, etc. --> + <attr name="templateHeaderButtonBackground" format="reference"/> + + <!-- The color of the header background for templates where it is used. --> + <attr name="templateHeaderBackgroundColor" format="reference"/> + + <!-- Theme attributes for rows in lists. --> + <!-- The background color of a row container view. + This color is used only for color contrast checking, and not for actual coloring of the grid item background. --> + <attr name="templateRowBackgroundColor" format="color"/> + + <!-- The background color of a row container view in the selected state.--> + <attr name="templateRowSelectedBackgroundColor" format="color"/> + + <!-- The color of the placeholder while an image is loading in a row.--> + <attr name="templateRowImagePlaceholderColor" format="color"/> + + <!-- Sign in Template --> + <attr name="templateSignInContainerStyle" format="reference"/> + <attr name="templateSignInMethodViewMaxWidth" format="dimension"/> + <attr name="templateSignInInstructionTextStyle" format="reference"/> + <attr name="templateSignInProviderSignInButtonStyle" format="reference"/> + <attr name="templateSignInPinTextStyle" format="reference"/> + <attr name="templateSignInPinBackgroundColor" format="color"/> + <attr name="templateSignInPinCornerRadius" format="dimension"/> + <attr name="templateSignInPinBackground" format="reference"/> + <attr name="templateSignInPinPadding" format="dimension"/> + <attr name="templateSignInQRCodeImageWidth" format="dimension"/> + <attr name="templateSignInAdditionalTextStyle" format="reference"/> + <attr name="templateSignInErrorMessageStyle" format="reference"/> + <attr name="templateSignInInputViewStyle" format="reference"/> + + <!-- Hyperlink Text --> + <attr name="templateHyperlinkTextColor" format="color"/> + + <!-- The default tint for an icon in a row. --> + <attr name="templateRowDefaultIconTint" format="color"/> + + <!-- The padding to the left and right of a row's contents. --> + <attr name="templateRowHorizontalPadding" format="dimension"/> + + <!-- The padding to the left and right of a half row's contents. --> + <attr name="templateRowHorizontalHalfPadding" format="dimension"/> + + <!-- The padding to the left and right of the text inside of a row. --> + <attr name="templateRowTextHorizontalPadding" format="dimension"/> + + <!-- The padding to the left and right of the text inside of a half row. --> + <attr name="templateRowTextHorizontalHalfPadding" format="dimension"/> + + <!-- The padding to the bottom of a half list's contents. --> + <attr name="templateHalfListBottomPadding" format="dimension"/> + + <!-- The vertical padding inside of a half row. --> + <attr name="templateHalfListPaddingVertical" format="dimension"/> + + <!-- The style of the title of a row. --> + <attr name="templateRowTitleStyle" format="reference"/> + + <!-- The style of the secondary text of a row. --> + <attr name="templateRowSecondaryTextStyle" format="reference"/> + + <!-- The style of the section header. --> + <attr name="templateRowSectionHeaderStyle" format="reference"/> + + <!-- The style of the text that indicates a list is empty. --> + <attr name="templateRowListEmptyTextStyle" format="reference"/> + + <!-- The image dimensions (for PaneTemplate) in the row list template. --> + <attr name="templateRowListToLargeImageRatio" format="dimension"/> + <attr name="templateRowListLargeImageContainerMaxWidth" format="dimension"/> + <attr name="templateRowListLargeImageAspectRatio" format="dimension"/> + + <!-- Padding between the (PaneTemplate) image and row list --> + <attr name="templateRowListAndImagePadding" format="dimension"/> + + <!-- The background for the top, middle, and bottom rows, and for a single + row that is both top and bottom, for lists that show rows with + a rounded corner background. These backgrounds can be used for + providing rounded corners to the list, or they can all simply be set + to the same background, if such an effect is not desired. --> + <attr name="templateRowBackgroundSectionalTop" format="reference"/> + <attr name="templateRowBackgroundSectionalMiddle" format="reference"/> + <attr name="templateRowBackgroundSectionalBottom" format="reference"/> + <attr name="templateRowBackgroundSectionalTopBottom" format="reference"/> + + <!-- The margin at the bottom of a section in a list. --> + <attr name="templateRowBackgroundSectionalBottomMargin" format="dimension"/> + + <!-- The placeholder for asynchronously loaded row images. --> + <attr name="templateRowImagePlaceholder" format="reference"/> + + <!-- The background for rows with square corners. --> + <attr name="templateRowBackgroundSimple" format="reference"/> + + <!-- The minimum margin between the rows's title and the edge + of the row. --> + <attr name="templateRowMinHeight" format="dimension"/> + + <!-- Attributes of the icons in a row of a list. --> + <attr name="templateRowIconStyle" format="reference"/> + <attr name="templateRowIconSize" format="dimension"/> + <attr name="templateRowRadioButtonSize" format="dimension"/> + <attr name="templateRowImageSizeSmall" format="dimension"/> + <attr name="templateRowImageSizeLarge" format="dimension"/> + + <!-- Attributes for the marker label that is displayed in the list rows + when the row has a location attached to it. --> + <!-- The minimum width of the marker label. The actual width can expand based on the content string.--> + <attr name="templateRowMarkerMinSize" format="dimension"/> + + <!-- The margin from the edge of the row to the marker label. --> + <attr name="templateRowMarkerLabelMargin" format="dimension"/> + + <!-- The height of a selection element's container in a row (e.g. a toggle + or radio button. --> + <attr name="templateRowSelectionContainerHeight" format="dimension"/> + + <!-- The half row minimum height. --> + <attr name="templateHalfRowMinHeight" format="dimension"/> + + <!-- The paddings used around the half row. --> + <attr name="templateHalfRowHorizontalPadding" format="dimension"/> + <attr name="templateHalfRowVerticalPadding" format="dimension"/> + + <!-- The paddings used around the full row. --> + <attr name="templateFullRowStartPadding" format="dimension"/> + <attr name="templateFullRowEndPadding" format="dimension"/> + + <!-- The spacing used between the image and text of the half row. --> + <attr name="templateHalfRowImageToTextSpacing" format="dimension"/> + + <!-- The spacing used between the primary and secondary text of the half row. --> + <attr name="templateHalfRowTextToTextSpacing" format="dimension"/> + + <!-- Size of the images used within the half list row. --> + <attr name="templateHalfRowImageSize" format="dimension"/> + + <!-- The full list row's chevron icon on the right side. --> + <attr name="templateFullRowChevronIcon" format="reference"/> + + <!-- The full list row's chevron height. --> + <attr name="templateFullRowChevronHeight" format="dimension"/> + + <!-- The full list row's chevron width. --> + <attr name="templateFullRowChevronWidth" format="dimension"/> + + <!-- The half list row's chevron icon on the right side. --> + <attr name="templateHalfRowChevronIcon" format="reference"/> + + <!-- The background for the grid item and its containers. --> + <attr name="templateGridItemBackground" format="reference"/> + + <!-- The background color for the grid item. + This color is used only for color contrast checking, and not for actual coloring of the grid item background. --> + <attr name="templateGridItemBackgroundColor" format="reference"/> + + <!-- The style of the title of a grid item. --> + <attr name="templateGridItemTitleStyle" format="reference"/> + + <!-- The style of the secondary text line below the title of a grid item. --> + <attr name="templateGridItemTextStyle" format="reference"/> + + <!-- The tint variations for an icon in a grid item. --> + <attr name="templateGridItemDefaultIconTint" format="color"/> + + <!-- The maximum width of a grid item text container. --> + <attr name="templateGridItemTextContainerMaxWidth" format="dimension"/> + + <!-- The bottom padding of the text inside of a grid item. --> + <attr name="templateGridItemTextBottomPadding" format="dimension"/> + + <!-- The bottom padding of the image inside of a grid item. --> + <attr name="templateGridItemImageBottomPadding" format="dimension"/> + + <!-- The padding for the grid items. --> + <attr name="templateGridItemHorizontalSpacing" format="dimension"/> + <attr name="templateGridItemVerticalSpacing" format="dimension"/> + + <!-- The number of grid items in a grid row. --> + <attr name="templateGridItemsPerRow" format="integer"/> + + <!-- Theme attributes for the grid. --> + <attr name="templateGridStyle" format="reference"/> + <attr name="templateGridRecyclerViewPaddingRight" format="reference"/> + + <!-- The style of the text that indicates a grid is empty. --> + <attr name="templateGridEmptyTextStyle" format="reference"/> + + <!-- Action buttons and FABs. --> + <!-- The margin between action buttons. --> + <attr name="templateActionButtonMargin" format="dimension"/> + + <!-- The style of an action displayed as a button. --> + <attr name="templateActionButtonStyle" format="reference"/> + + <!-- The style of the text inside of an action button. --> + <attr name="templateActionButtonTextStyle" format="reference"/> + + <!-- The style of the text inside of a primary action button. --> + <attr name="templateActionButtonPrimaryTextStyle" format="reference"/> + + <!-- The default background color of an action displayed as a button. --> + <attr name="templateActionButtonDefaultBackgroundColor" format="color" /> + + <!-- The background color of a primary action displayed as a button. --> + <attr name="templateActionButtonPrimaryBackgroundColor" format="color" /> + + <!-- The default foreground drawable of an action displayed as a button. --> + <attr name="templateActionButtonForeground" format="reference" /> + + <!-- The default background drawable of an action displayed as a button. --> + <attr name="templateActionButtonBackground" format="reference" /> + + <!-- The height of an action button. --> + <attr name="templateActionButtonHeight" format="dimension"/> + + <!-- The minimum touch area size for action buttons. --> + <attr name="templateActionButtonTouchTargetSize" format="dimension"/> + + <!-- Whether buttons in the action button list (e.g. used in PaneTemplate) stretch to fill the horizontal space. --> + <attr name="templateActionButtonListButtonStretchHorizontal" format="boolean"/> + + <!-- Whether OEM colors should override 3P provided colors. --> + <attr name="templateActionButtonUseOemColors" format="boolean"/> + + <!-- The alignment of contents in buttons in the action button list (e.g. used in PaneTemplate). + The possible values are: + <ul> + <li>0: center (default) + <li>1: left + <li>2: right + </ul> --> + <attr name="templateActionButtonPrimaryHorizontalOrder" format="integer"/> + + <!-- The gravity of action button list (e.g. used in MessageTemplate, SigninTemplate and LongMessageTemplate). + The possible values are: + <ul> + <li>0: center (default) + <li>1: bottom + </ul> --> + <attr name="templateActionButtonListGravity" format="integer"/> + <!-- The alignment of contents in buttons in the action button list (e.g. used in PaneTemplate). + The possible values are: + <ul> + <li>0: center (default) + <li>1: left + <li>2: right + </ul> --> + <attr name="templateActionButtonListButtonContentAlignment" format="integer"/> + + <!-- The maximum width of a button in the action button list, e.g. used in PaneTemplate. + This value will be used only when `templateActionButtonListFillHorizontalSpace` is set to `true`. --> + <attr name="templateActionButtonListButtonMaxWidth" format="dimension"/> + + <!-- The spacing between the content and the aligned side in a button in the action button list, e.g. used in PaneTemplate. + This value will be used only when `templateActionButtonListButtonContentAlignment` is set to 1 (left) or 2 (right). + When this value is used, `templateActionIconTextStartSpacing`, `templateActionIconTextEndSpacing`, and `templateActionTextHorizontalSpacing` will be ignored. --> + <attr name="templateActionButtonSideAlignmentSpacing" format="dimension"/> + + <!-- The vertical spacing of the action button list row, e.g. used in PaneTemplate. --> + <attr name="templateActionButtonListRowVerticalSpacing" format="dimension"/> + + <!-- The size of an icon inside of an action button or FAB. --> + <attr name="templateActionIconSize" format="dimension"/> + <attr name="templateActionIconSizeMin" format="dimension"/> + <attr name="templateActionIconSizeMax" format="dimension"/> + + <!-- The spacing between the start side of a FAB or button and the action icon. + The spacing is applied only when the button has both icon and text. --> + <attr name="templateActionIconTextStartSpacing" format="dimension"/> + + <!-- The spacing between the end side of a FAB or button and the action icon. + The spacing is applied only when the button has both icon and text. --> + <attr name="templateActionIconTextEndSpacing" format="dimension"/> + + <!-- The spacing between the icon and the text in a FAB or button. --> + <attr name="templateActionIconToTextSpacing" format="dimension"/> + + <!-- The spacing between the start and end sides of a FAB or button and the action text. + The spacing is applied only when the button only has the text. --> + <attr name="templateActionTextHorizontalSpacing" format="dimension"/> + + <!-- The default tint of an icon inside of an action button or FAB. --> + <attr name="templateActionDefaultIconTint" format="color"/> + + <!-- The min width of an action button or FAB with text. --> + <attr name="templateActionWithTextMinWidth" format="integer"/> + + <!-- The min width of an action button or FAB with an icon only. --> + <attr name="templateActionWithoutTextMinWidth" format="dimension"/> + + <!-- The max ems of the text inside of a button when there is no icon. --> + <attr name="templateActionButtonTextMaxEmsNoIcon" format="integer"/> + + <!-- The max ems of the text inside of a button when there is an icon. --> + <attr name="templateActionButtonTextMaxEmsWithIcon" format="integer"/> + + <!-- The max ems of the text inside of a FAB when there is no icon. --> + <attr name="templateFabTextMaxEmsNoIcon" format="integer"/> + + <!-- The max ems of the text inside of a FAB when there is an icon. --> + <attr name="templateFabTextMaxEmsWithIcon" format="integer"/> + + <!-- The width of the border of a secondary button. --> + <attr name="templateActionButtonSecondaryBorderWidth" format="dimension"/> + + <!-- The color of the border of a secondary button. --> + <attr name="templateActionButtonSecondaryBorderColor" format="color"/> + + <!-- The margin between buttons in the action strip. --> + <attr name="templateActionStripButtonMargin" format="dimension"/> + + <!-- The padding around the action strip. --> + <attr name="templateActionStripPadding" format="dimension"/> + + <!-- The color of buttons in the action strip in full-screen templates. --> + <attr name="templateActionStripButtonBackgroundColor" format="color"/> + + <!-- The light color of buttons in the action strip as FABs. --> + <attr name="templateActionStripFabBackgroundColorLight" format="color"/> + + <!-- The dark color of buttons in the action strip as FABs. --> + <attr name="templateActionStripFabBackgroundColorDark" format="color"/> + + <!-- The appearance of action strip buttons as FABs. --> + <attr name="templateActionStripFabAppearance" format="reference"/> + + <!-- The appearance of action strip buttons in full-screen templates. --> + <attr name="templateActionStripFullTemplateFabAppearance" format="reference"/> + + <!-- Ripple attributes, common for all elements using ripples. --> + <attr name="templateRippleColor" format="color"/> + <attr name="templateRippleSelectorColor" format="color"/> + + <!-- Toggles and radio buttons. --> + <attr name="templateToggleWidth" format="dimension"/> + <attr name="templateToggleHeight" format="dimension"/> + <attr name="templateToggleInactiveTrackColor" format="color"/> + <attr name="templateToggleInactiveThumbColor" format="color"/> + <attr name="templateToggleActiveTrackColor" format="color"/> + <attr name="templateToggleActiveThumbColor" format="color"/> + <attr name="templateRadioButtonSize" format="dimension"/> + + <!-- Clickable spans. --> + <attr name="templateClickableSpanHighlightForegroundColor" format="color"/> + <attr name="templateClickableSpanHighlightBackgroundColor" format="color"/> + + <!-- Focus. --> + <attr name="templateFocusAccentColor" format="color"/> + <attr name="templateFocusNoContentAccentColor" format="color"/> + <attr name="templateFocusStateInactive" format="boolean"/> + <attr name="templateFocusRingColor" format="color"/> + <attr name="templateFocusRingNoAccentColor" format="color"/> + + <!-- Editable text. --> + <attr name="templateEditTextStyle" format="reference"/> + <attr name="templateEditTextActiveColor" format="color"/> + <attr name="templateEditTextEnabledColor" format="color"/> + <attr name="templateEditTextErrorColor" format="color"/> + <attr name="templateEditTextDisabledColor" format="color"/> + <attr name="templateEditTextErrorVerticalSpacing" format="dimension"/> + <attr name="templateEditTextErrorHorizontalSpacing" format="dimension"/> + + <!-- Search bar. --> + <!-- Maximum width of the search bar. --> + <attr name="templateSearchBarMaxWidth" format="dimension"/> + + <!-- The search icon on the left side of the search bar. --> + <attr name="templateSearchBarIcon" format="reference"/> + + <!-- Images. --> + <!-- The size of a large image. --> + <attr name="templateLargeImageSize" format="dimension"/> + + <!-- The minimum size of a large image. --> + <attr name="templateLargeImageSizeMin" format="dimension"/> + + <!-- The maximum size of a large image. --> + <attr name="templateLargeImageSizeMax" format="dimension"/> + + <!-- Message template. --> + <!-- The default tint of an icon inside of the message template. --> + <attr name="templateMessageDefaultIconTint" format="color"/> + + <!-- The style of the text in the title of the message template. --> + <attr name="templateMessageTitleTextStyle" format="reference"/> + + <!-- The spacing used on top of the title of the message template. --> + <attr name="templateMessageTitleTopSpacing" format="dimension"/> + + <!-- The spacing used on top of the buttons of the message template. --> + <attr name="templateMessageButtonsTopSpacing" format="dimension"/> + + <!-- The spacing used on top and bottom of the sticky buttons. --> + <attr name="templateStickyButtonsVerticalSpacing" format="dimension"/> + + <!-- The style of the text in the long message template. --> + <attr name="templateMessageLongTextStyle" format="reference"/> + + <!-- The color of the text and background of the debug message showing the + callstack in error screens. --> + <attr name="templateMessageDebugTextStyle" format="reference"/> + <attr name="templateDebugMessageBackgroundColor" format="color"/> + + <!-- Navigation routing template. --> + <!-- The maximum heights and width of an image in a text span. + body2 and body3 variants should be used along text views configured + with those respective font sizes. --> + <attr name="templateRoutingImageSpanBody2MaxHeight" format="dimension"/> + <attr name="templateRoutingImageSpanBody3MaxHeight" format="dimension"/> + + <!-- The horizontal and vertical padding values in the routing card. --> + <attr name="templateNavCardPaddingHorizontal" format="dimension"/> + <attr name="templateNavCardPaddingVertical" format="dimension"/> + + <!-- The small vertical padding value in the routing card and the travel estimate card. --> + <attr name="templateNavCardSmallPaddingVertical" format="dimension"/> + + <!-- The horizontal padding value between the icon and the distance text in the routing card. --> + <attr name="templateRoutingStepsCardIconToDistanceSpacingHorizontal" format="dimension"/> + + <!-- Ratio of the image span (width/height) based on the max height value --> + <attr name="templateRoutingImageSpanRatio" format="float"/> + + <!-- The dimensions of the large image in the routing card (ratio 1:1). --> + <attr name="templateNavCardLargeImageSize" format="dimension"/> + <attr name="templateNavCardLargeImageSizeMin" format="dimension"/> + <attr name="templateNavCardLargeImageSizeMax" format="dimension"/> + + <!-- The dimensions of the small image in the routing card (ratio 1:1). --> + <attr name="templateNavCardSmallImageSize" format="dimension"/> + <attr name="templateNavCardSmallImageSizeMin" format="dimension"/> + <attr name="templateNavCardSmallImageSizeMax" format="dimension"/> + + <!-- Size of the large text of the routing card. --> + <attr name="templateNavCardLargeTextSize" format="dimension"/> + + <!-- Size of the xlarge text of the routing card. --> + <attr name="templateNavCardXLargeTextSize" format="dimension"/> + + <!-- The fallback text color used in the routing card when the OEM-provided default text color is not used. --> + <attr name="templateNavCardFallbackContentColor" format="color"/> + + <!-- The style of the distance text in the detailed step view. --> + <attr name="templateRoutingDistanceStyle" format="dimension"/> + + <!-- The style of the description text in the detailed step view. --> + <attr name="templateRoutingDescriptionStyle" format="dimension"/> + + <!-- The style of the description text in the compact step view. --> + <attr name="templateRoutingCompactDescriptionStyle" format="dimension"/> + + <!-- The style of the description text in the travel estimate view. --> + <attr name="templateRoutingTravelEstimateStyle" format="dimension"/> + + <!-- The height of the container of the lanes image. --> + <attr name="templateRoutingLanesImageContainerHeight" format="dimension"/> + + <!-- The vertical padding of the container of the lanes image. --> + <attr name="templateRoutingLanesImageContainerVerticalPadding" format="dimension"/> + + <!-- The horizontal padding of the container of the lanes image. --> + <attr name="templateRoutingLanesImageContainerHorizontalPadding" format="dimension"/> + + <!-- The color of the background of the lanes image. --> + <attr name="templateRoutingLanesImageBackgroundColor" format="color"/> + + <!-- The color of the background of the junction image. --> + <attr name="templateRoutingJunctionImageBackgroundColor" format="color"/> + + <!-- The style of the primary text for the title in the message view. --> + <attr name="templateRoutingMessagePrimaryStyle" format="reference"/> + + <!-- The style of the secondary text for the message view. --> + <attr name="templateRoutingMessageSecondaryStyle" format="reference"/> + + <!-- The horizontal inner padding between the image and the primary text in the message view. --> + <attr name="templateRoutingMessageInnerPaddingHorizontal" format="dimension"/> + + <!-- The vertical inner padding between the primary and secondary texts in the message view. --> + <attr name="templateRoutingMessageInnerPaddingVertical" format="dimension"/> + + <!-- The width and min height of the container which shows the current and + next steps in the routing card. --> + <attr name="templateRoutingStepsCardContentContainerMinWidth" format="dimension"/> + <attr name="templateRoutingStepsCardContentContainerMinHeight" format="dimension"/> + + <!-- The color of the divider in the routing card. --> + <attr name="templateRoutingDividerColor" format="dimension"/> + + <!-- Status bar. --> + <!-- Status bar gradient background start and end colors. --> + <attr name="templateStatusBarStartColor" format="color"/> + <attr name="templateStatusBarEndColor" format="color"/> + + <!-- Defines a minimum top padding for the presenter in case there is no status bar, + i.e. Widescreen Android Auto does not have status bar. --> + <attr name="templateStatusBarMinimumTopPadding" format="dimension"/> + + <!-- No content view. --> + <attr name="templateNoContentFocusCornerRadius" format="dimension"/> + + <!-- Loading spinner. --> + <attr name="templateLoadingSpinnerStyle" format="reference"/> + <attr name="templateLoadingSpinnerColor" format="color"/> + + <!-- Attributes of most dividers used throughout the UI. --> + <attr name="templateDividerColor" format="color"/> + <attr name="templateDividerThickness" format="dimension"/> + + <!-- A fraction used for implementing margins that adapt to the width of the screen. + For example, some templates may have a 12% minimum margin (with respect of the + screen width) on either side. This value should be set to 1.0 minus twice + the margin percentage (in other words, it is the width of the content itself). --> + <attr name="templateAdaptiveWidthFraction" format="float"/> + + <!-- The duration in milliseconds of a template transition animation e.g. a cross fade. --> + <attr name="templateUpdateAnimationDurationMilliseconds" format="integer"/> + + <!-- Standard colors --> + <attr name="templateStandardBlue" format="color"/> + <attr name="templateStandardRed" format="color"/> + <attr name="templateStandardGreen" format="color"/> + <attr name="templateStandardYellow" format="color"/> + + <!-- The spacing used between controls e.g. buttons. --> + <attr name="templateControlToControlSpacingHorizontal" format="dimension"/> + + <!-- The maximum number of rows in a list view. --> + <attr name="templateListMaxLength" format="integer"/> + + <!-- The maximum number of grid items in a grid view. --> + <attr name="templateGridMaxLength" format="integer"/> + + <!-- Whether or not NavState events should be sent to the system via NavigationManager --> + <attr name="templateSendNavStateToSystem" format="boolean"/> + </declare-styleable> +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/colors.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/colors.xml new file mode 100644 index 0000000..31833f1 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/colors.xml @@ -0,0 +1,137 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!-- Color definitions for the UI in the templates. + + !!! IMPORTANT !!! + Do not refer to these colors directly from views. Colors must be referred + to through theme attributes (in attrs.xml). + + !!! IMPORTANT !!! + Do not define colors with a RGB value in here (e.g. #FF9B4DF8). These + colors should all refer to colors defined in @color/default. + + If you need a color that's not there already, make a change to add it and + get that approved. --> +<resources> + + <color name="template_black">@color/default_black</color> + <color name="template_black_64">@color/default_gradient_black_64</color> + + <color name="template_white">@color/default_white</color> + <color name="template_white_16">@color/default_gradient_white_16</color> + <color name="template_white_24">@color/default_gradient_white_24</color> + <color name="template_white_46">@color/default_gradient_white_46</color> + <color name="template_white_56">@color/default_gradient_white_56</color> + <color name="template_white_72">@color/default_gradient_white_72</color> + + <color name="template_gray_50">@color/default_gray_50</color> + <color name="template_gray_100">@color/default_gray_100</color> + <color name="template_gray_400">@color/default_gray_400</color> + <color name="template_gray_500">@color/default_gray_500</color> + <color name="template_gray_700">@color/default_gray_700</color> + <color name="template_gray_846">@color/default_gray_846</color> + <color name="template_gray_868">@color/default_gray_868</color> + <color name="template_gray_878">@color/default_gray_878</color> + <color name="template_gray_900">@color/default_gray_900</color> + <color name="template_gray_928">@color/default_gray_928</color> + + <!-- Standard colors --> + <color name="template_standard_red">@color/car_app_ui_standard_red</color> + <color name="template_standard_red_dark">@color/car_app_ui_standard_red_dark</color> + <color name="template_standard_green">@color/car_app_ui_standard_green</color> + <color name="template_standard_green_dark">@color/car_app_ui_standard_green_dark</color> + <color name="template_standard_blue">@color/car_app_ui_standard_blue</color> + <color name="template_standard_blue_dark">@color/car_app_ui_standard_blue_dark</color> + <color name="template_standard_yellow">@color/car_app_ui_standard_yellow</color> + <color name="template_standard_yellow_dark">@color/car_app_ui_standard_yellow_dark</color> + + <!-- Colors derived from OEM values. These values should be customized + through car_ui. --> + <color name="template_icon_tint_color">@color/car_ui_text_color_primary</color> + <color name="template_text_color_primary">@color/car_ui_text_color_primary</color> + <color name="template_card_background_color">@color/car_ui_activity_background_color</color> + <color name="template_plain_content_background_color">@color/car_ui_activity_background_color</color> + + <!-- Color of text inside of a content view. --> + <color name="template_content_text_color">@color/template_gray_50</color> + + <!-- Color of secondary text inside of a content view. --> + <color name="template_content_text_color_secondary">@color/template_gray_500</color> + + <!-- Color of focus text inside of a content view. --> + <color name="template_content_text_color_focus">@color/template_standard_blue</color> + + <!-- Map markers. --> + <color name="template_marker_default_background_color">@color/template_white</color> + <color name="template_marker_map_default_content_color">@color/template_black</color> + <color name="template_marker_list_default_content_color">@color/template_white</color> + <color name="template_marker_custom_background_content_color">@color/template_white</color> + <color name="template_marker_default_border_color">@color/template_gray_700</color> + <color name="template_marker_custom_border_color">@color/template_white</color> + <color name="template_anchor_default_background_color">@color/template_black</color> + <color name="template_anchor_border_color">@color/template_white</color> + <color name="template_anchor_dot_color">@color/template_white_56</color> + + <!-- Content Containers. --> + <color name="template_container_background">@color/template_black</color> + <color name="template_content_button_color">@color/template_white</color> + + <!-- Cards. --> + <color name="template_card_content_container_border_color">@color/template_white_24</color> + + <!-- Toggles. --> + <color name="template_toggle_inactive_track">@color/template_white</color> + <color name="template_toggle_inactive_thumb">@color/template_gray_400</color> + <color name="template_toggle_active_track">@color/template_standard_blue</color> + <color name="template_toggle_active_thumb">@color/template_standard_blue</color> + + <!-- Actions. --> + <color name="template_action_button_default_background_color">@color/car_app_ui_action_button_default_background_color</color> + + <!-- Action strip FABs. --> + <color name="template_action_strip_fab_background_color">@color/car_app_ui_floating_button_default_background_color</color> + <color name="template_action_strip_fab_content_color">@color/car_app_ui_floating_button_default_text_color</color> + + <!-- Message template. --> + <color name="template_message_debug_text_color">@color/default_message_debug_text_color</color> + + <!-- Sign-in template. --> + <color name="template_sign_in_error_message_color">@color/template_standard_red</color> + <color name="template_sign_in_additional_text_color">@color/template_gray_500</color> + + <!-- Read-only text. --> + <color name="template_read_only_text_background_color">@color/car_app_ui_read_only_text_background_color</color> + + <!-- Ripples. --> + <color name="template_ripple_color">@color/default_controller_ripple_color</color> + <color name="template_ripple_selector_color">@color/default_controller_ripple_selector_color</color> + + <!-- Status bar. --> + <color name="template_status_bar_end_color">@color/template_white</color> + + <!-- Edit text. --> + <color name="template_edit_text_color_selector">@color/default_edit_text_color_selector</color> + <color name="template_edit_text_active_color">@color/car_app_ui_edit_text_active_color</color> + <color name="template_edit_text_enabled_color">@color/car_app_ui_edit_text_enabled_color</color> + <color name="template_edit_text_error_color">@color/car_app_ui_edit_text_error_color</color> + <color name="template_edit_text_disabled_color">@color/car_app_ui_edit_text_disabled_color</color> + + <!-- Loading spinner. --> + <color name="template_loading_spinner_color">@color/template_standard_blue</color> + +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/dimens.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/dimens.xml new file mode 100644 index 0000000..defd4c4 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/dimens.xml @@ -0,0 +1,278 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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> + + <!-- Adaptive height and width spacing key lines. --> + <dimen name="template_height_keyline_0">@dimen/default_height_keyline_0</dimen> + <dimen name="template_height_keyline_1">@dimen/default_height_keyline_1</dimen> + <dimen name="template_height_keyline_2">@dimen/default_height_keyline_2</dimen> + <dimen name="template_height_keyline_3">@dimen/default_height_keyline_3</dimen> + <dimen name="template_width_keyline_0">@dimen/default_width_keyline_0</dimen> + <dimen name="template_width_keyline_1">@dimen/default_width_keyline_1</dimen> + <dimen name="template_width_keyline_2">@dimen/default_width_keyline_2</dimen> + <dimen name="template_width_keyline_3">@dimen/default_width_keyline_3</dimen> + + <!-- Padding. --> + <dimen name="template_padding_0">@dimen/default_padding_0</dimen> + <dimen name="template_padding_1">@dimen/default_padding_1</dimen> + <dimen name="template_padding_2">@dimen/default_padding_2</dimen> + <dimen name="template_padding_3">@dimen/default_padding_3</dimen> + <dimen name="template_padding_4">@dimen/default_padding_4</dimen> + <dimen name="template_padding_5">@dimen/default_padding_5</dimen> + <dimen name="template_padding_6">@dimen/default_padding_6</dimen> + <dimen name="template_padding_7">@dimen/default_padding_7</dimen> + <dimen name="template_padding_8">@dimen/default_padding_8</dimen> + + <!-- Minimum tap target size, used for buttons, etc. --> + <dimen name="template_min_tap_target_size">68dp</dimen> + + <!-- + Edge column definition + + These values are used to align UI elements at the edge of the screen, even if + the elements belong to completely different layouts. + --> + <dimen name="template_edge_column_width">@dimen/default_edge_column_width</dimen> + <dimen name="template_edge_column_margin">@dimen/default_edge_column_margin</dimen> + + <!-- + This dimen represents the entire width of the edge column, margin included. + + It should remain equal to: + 2 * template_edge_column_margin + template_edge_column_width + --> + <dimen name="template_edge_column_width_padded">@dimen/default_edge_column_width_padded</dimen> + + <!-- A minimum margin value used to offset some of the text views + in the UI. --> + <dimen name="template_min_text_margin">8dp</dimen> + + <!-- Common dimensions. --> + <item name="template_adaptive_width_fraction" format="float" type="dimen">.76</item> + <dimen name="template_button_touch_target_size">@dimen/car_app_ui_touch_target_size</dimen> + + <!-- Images. --> + <dimen name="template_large_image_size">@dimen/car_app_ui_large_image_size</dimen> + <dimen name="template_large_image_size_min">0dp</dimen> + <dimen name="template_large_image_size_max">128dp</dimen> + + <!-- Markers. --> + <dimen name="template_marker_pointer_width">16dp</dimen> + <dimen name="template_marker_pointer_height">10dp</dimen> + <dimen name="template_marker_text_horizontal_padding">8dp</dimen> + <dimen name="template_marker_icon_size">32dp</dimen> + <dimen name="template_marker_stroke">2dp</dimen> + <dimen name="template_marker_corner_radius">8dp</dimen> + <dimen name="template_marker_image_size">36dp</dimen> + <dimen name="template_marker_image_corner_radius">4dp</dimen> + <dimen name="template_marker_padding">2dp</dimen> + + <!-- Plain content containers. --> + <dimen name="template_plain_content_container_width">400dp</dimen> + <dimen name="template_plain_content_container_padding">10dp</dimen> + <dimen name="template_plain_content_horizontal_padding">@dimen/car_app_ui_content_horizontal_margin</dimen> + + <!-- Card content containers. --> + <dimen name="template_card_content_container_default_width">384dp</dimen> + <dimen name="template_card_content_container_min_width">360dp</dimen> + <dimen name="template_card_content_container_max_width">416dp</dimen> + <dimen name="template_card_content_container_oem_max_width">500dp</dimen> + <dimen name="template_card_content_container_start_margin">@dimen/car_app_ui_card_start_margin</dimen> + <dimen name="template_card_content_container_top_margin">@dimen/car_app_ui_card_top_margin</dimen> + <dimen name="template_card_content_container_bottom_margin">@dimen/template_width_keyline_1</dimen> + <dimen name="template_card_content_container_min_height">200dp</dimen> + + <!-- Card container width fractions. Zero means we use the layout_width, not the fraction. --> + <item name="template_card_content_container_width_fraction" format="float" type="dimen">.48</item> + <item name="template_steps_card_content_container_width_fraction" format="float" type="dimen">.4</item> + + <!-- Corresponds to +5 elevation in material spec: + https://standards.google/guidelines/google-material/styles/elevation.html#spec --> + <dimen name="template_card_content_container_elevation">12dp</dimen> + <dimen name="template_card_content_container_border_width">2dp</dimen> + + <!-- Headers. --> + <dimen name="template_header_height">68dp</dimen> + <dimen name="template_header_text_vertical_spacing">@dimen/car_app_ui_card_header_text_padding_vertical</dimen> + <dimen name="template_header_text_horizontal_spacing">@dimen/car_app_ui_card_header_text_padding_horizontal</dimen> + <dimen name="template_header_button_icon_size">@dimen/car_app_ui_card_header_image_size</dimen> + <dimen name="template_header_button_start_spacing">0dp</dimen> + <dimen name="template_header_text_no_icon_start_spacing">@dimen/car_app_ui_card_header_no_button_text_margin_start</dimen> + + <dimen name="template_header_button_focus_inset">4dp</dimen> + + <!-- Lists. --> + <dimen name="template_row_list_max_width">@dimen/default_canvas_max_width</dimen> + <dimen name="template_row_list_no_scrollbar_start_padding_card">@dimen/template_padding_1</dimen> + + <!-- Row List. --> + <dimen name="template_row_list_large_image_container_max_width">480dp</dimen> + + <!-- Rows. --> + <dimen name="template_row_min_height">72dp</dimen> + <dimen name="template_row_corner_radius">@dimen/default_list_item_corner_radius</dimen> + <dimen name="template_row_icon_size">36dp</dimen> + <dimen name="template_row_image_size_small">36dp</dimen> + <dimen name="template_row_image_size_large">64dp</dimen> + <dimen name="template_row_action_max_width">240dp</dimen> + <dimen name="template_row_marker_min_size">36dp</dimen> + <dimen name="template_row_marker_label_margin">15dp</dimen> + <dimen name="template_row_horizontal_padding">@dimen/template_width_keyline_1</dimen> + <dimen name="template_row_horizontal_half_padding">@dimen/template_width_keyline_0</dimen> + <dimen name="template_row_text_horizontal_padding">@dimen/template_width_keyline_0</dimen> + <dimen name="template_row_text_horizontal_half_padding">@dimen/template_padding_0</dimen> + <dimen name="template_row_background_sectional_bottom_margin">@dimen/template_padding_3</dimen> + <dimen name="template_half_list_bottom_padding">@dimen/default_height_keyline_3</dimen> + <dimen name="template_half_row_padding_vertical">@dimen/default_padding_4</dimen> + + <!-- Half Rows. --> + <dimen name="template_half_row_horizontal_padding">@dimen/car_app_ui_half_row_horizontal_padding</dimen> + <dimen name="template_half_row_vertical_padding">@dimen/car_app_ui_half_row_vertical_padding</dimen> + <dimen name="template_half_row_image_to_text_margin">@dimen/car_app_ui_half_row_image_to_text_spacing</dimen> + <dimen name="template_half_row_text_to_text_margin">@dimen/car_app_ui_half_row_text_to_text_spacing</dimen> + <dimen name="template_half_row_image_size">@dimen/car_app_ui_half_row_image_size</dimen> + + <!-- Full Rows. --> + <dimen name="template_full_row_start_padding">@dimen/car_app_ui_full_row_start_padding</dimen> + <dimen name="template_full_row_end_padding">@dimen/car_app_ui_full_row_end_padding</dimen> + <dimen name="template_full_row_chevron_size">@dimen/car_ui_list_item_supplemental_icon_size</dimen> + + <!-- Grids. --> + <dimen name="template_grid_max_width">@dimen/default_paged_list_view_max_content_width</dimen> + + <!-- Grid items. --> + <dimen name="template_grid_item_image_bottom_padding">@dimen/car_app_ui_grid_item_image_to_text_spacing_vertical</dimen> + <dimen name="template_grid_item_text_container_max_width">196dp</dimen> + <dimen name="template_grid_item_text_bottom_padding">@dimen/car_app_ui_grid_item_text_to_text_spacing_vertical</dimen> + <dimen name="template_grid_item_horizontal_spacing">@dimen/template_width_keyline_0</dimen> + <dimen name="template_grid_item_vertical_spacing">@dimen/car_app_ui_grid_item_vertical_spacing</dimen> + + <dimen name="template_grid_item_corner_radius">@dimen/default_list_item_corner_radius</dimen> + <dimen name="template_grid_item_image_background_size">50dp</dimen> + + <!-- Sign-In Template. --> + <dimen name="template_sign_in_method_view_max_width">@dimen/car_app_ui_sign_in_method_max_width</dimen> + <dimen name="template_sign_in_pin_text_letter_spacing" format="float" type="dimen">.6</dimen> + <dimen name="template_sign_in_input_error_message_text_size">18dp</dimen> + <dimen name="template_sign_in_input_additional_text_size">18dp</dimen> + <dimen name="template_sign_in_qr_code_image_width">250dp</dimen> + + <!-- Read-only Text. --> + <dimen name="template_read_only_text_padding">@dimen/car_app_ui_read_only_text_padding</dimen> + + <!-- Scrollbar. --> + <dimen name="template_paged_list_scrollbar_width">@dimen/template_edge_column_width_padded</dimen> + <dimen name="template_paged_list_scrollbar_width_card">68dp</dimen> + + <!-- Dividers. --> + <dimen name="template_divider_thickness">1dp</dimen> + + <!-- Action buttons and FABs. --> + <!-- Vertical margin in action button. --> + <dimen name="template_action_button_vertical_margin">@dimen/template_padding_2</dimen> + + <!-- Action button list row vertical spacing. --> + <dimen name="template_action_button_list_row_vertical_spacing">@dimen/template_padding_2</dimen> + + <!-- Min width common to buttons and FABs, when the action has text. --> + <dimen name="template_action_with_text_min_width">156dp</dimen> + + <!-- The size of an icon inside of a FAB or button. --> + <dimen name="template_action_icon_size">@dimen/car_app_ui_button_image_size</dimen> + <dimen name="template_action_icon_size_min">0dp</dimen> + <!-- This max size should align with the max size we can set on the system toolbar. --> + <dimen name="template_action_icon_size_max">88dp</dimen> + + <!-- The spacing between the start side of a FAB or button and the action icon. + The spacing is applied only when the button has both icon and text. --> + <dimen name="template_action_icon_text_start_spacing">@dimen/car_app_ui_icon_button_start_spacing</dimen> + + <!-- The spacing between the end side of a FAB or button and the action icon. + The spacing is applied only when the button has both icon and text. --> + <dimen name="template_action_icon_text_end_spacing">@dimen/car_app_ui_icon_button_end_spacing</dimen> + + <!-- The spacing between the icon and the text in a FAB or button. --> + <dimen name="template_action_icon_to_text_spacing">@dimen/car_app_ui_icon_button_image_to_text_spacing</dimen> + + <!-- The spacing between the start and end sides of a FAB or button and the action text. + The spacing is applied only when the button only has the text. --> + <dimen name="template_action_text_horizontal_spacing">@dimen/car_app_ui_button_text_horizontal_spacing</dimen> + + <!-- The width of the border for a secondary button, e.g. a button rendered + in the action strip in a full screen template. --> + <dimen name="template_action_button_secondary_border_width">2dp</dimen> + + <!-- The padding around the action strip. --> + <dimen name="template_action_strip_padding">@dimen/car_ui_padding_3</dimen> + + <!-- The vertical margin of the sticky buttons. --> + <dimen name="template_sticky_buttons_vertical_spacing">@dimen/car_ui_padding_2</dimen> + + <!-- Toggles and radio buttons. --> + <dimen name="template_toggle_width">44dp</dimen> + <dimen name="template_toggle_height">24dp</dimen> + <dimen name="template_radio_button_size">24dp</dimen> + + <!-- Focus. --> + <dimen name="template_focus_ring_stroke_width_hovered">@dimen/default_focus_ring_stroke_width_hovered</dimen> + <dimen name="template_focus_ring_stroke_width_focused">@dimen/default_focus_ring_stroke_width_focused</dimen> + <dimen name="template_back_focus_ring_inset">4dp</dimen> + <dimen name="template_search_focus_ring_inset">6dp</dimen> + <dimen name="template_action_fab_focus_ring_size">56dp</dimen> + <dimen name="template_focus_oval_ripple_inset">4dp</dimen> + + <!-- Routing. --> + <dimen name="template_routing_image_span_body2_max_height">@dimen/default_body2_line_height</dimen> + <dimen name="template_routing_image_span_body3_max_height">@dimen/default_body3_line_height</dimen> + <dimen name="template_nav_card_large_image_size">@dimen/car_app_ui_nav_card_large_image_size</dimen> + <dimen name="template_nav_card_large_image_size_min">0dp</dimen> + <dimen name="template_nav_card_large_image_size_max">128dp</dimen> + <dimen name="template_nav_card_small_image_size">@dimen/car_app_ui_nav_card_small_image_size</dimen> + <dimen name="template_nav_card_small_image_size_min">0dp</dimen> + <dimen name="template_nav_card_small_image_size_max">72dp</dimen> + <dimen name="template_routing_lanes_image_container_height">55dp</dimen> + + <dimen name="template_nav_card_padding_vertical">@dimen/car_app_ui_nav_card_padding_vertical</dimen> + <dimen name="template_nav_card_padding_horizontal">@dimen/car_app_ui_nav_card_padding_horizontal</dimen> + <dimen name="template_nav_card_small_padding_vertical">@dimen/car_app_ui_nav_card_small_padding_vertical</dimen> + <dimen name="template_steps_card_image_to_text_spacing_horizontal">@dimen/car_app_ui_nav_card_image_to_text_spacing_horizontal</dimen> + <dimen name="template_steps_card_image_to_text_spacing_vertical">@dimen/car_app_ui_nav_card_image_to_text_spacing_vertical</dimen> + + <dimen name="template_steps_card_content_container_min_width">320dp</dimen> + <dimen name="template_steps_card_content_container_max_width">372dp</dimen> + <dimen name="template_steps_card_content_container_oem_max_width">500dp</dimen> + <dimen name="template_steps_card_content_container_min_height">100dp</dimen> + + <!-- Message template. --> + <dimen name="template_message_title_top_spacing">@dimen/car_app_ui_image_to_text_spacing_vertical</dimen> + <dimen name="template_message_buttons_top_spacing">@dimen/car_app_ui_text_to_control_spacing_vertical</dimen> + <dimen name="template_message_debug_text_size">14dp</dimen> + <dimen name="template_message_debug_text_line_height">18dp</dimen> + + <!-- Search template. --> + <dimen name="template_search_bar_max_width">580dp</dimen> + + <!-- Status bar. --> + <dimen name="template_status_bar_minimum_top_padding">0dp</dimen> + + <!-- No content view. --> + <dimen name="template_no_content_view_focus_corner_radius">@dimen/default_no_content_focus_background_corner_radius</dimen> + + <!-- Use a negative padding value to draw the focus ring outside the no content view. --> + <dimen name="template_no_content_view_focus_ring_padding">-8dp</dimen> +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/dimens_default.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/dimens_default.xml new file mode 100644 index 0000000..d462eba --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/dimens_default.xml @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!-- Standard dimensions for all template widgets. + + These dimensions can be referenced directly from widgets without going + through a theme attribute. + + See go/watevra-visd. --> +<resources> + + <dimen name="default_padding_0">4dp</dimen> + <dimen name="default_padding_1">8dp</dimen> + <dimen name="default_padding_2">12dp</dimen> + <dimen name="default_padding_3">16dp</dimen> + <dimen name="default_padding_4">24dp</dimen> + <dimen name="default_padding_5">32dp</dimen> + <dimen name="default_padding_6">48dp</dimen> + <dimen name="default_padding_7">64dp</dimen> + <dimen name="default_padding_8">96dp</dimen> + + <dimen name="default_height_keyline_0">0dp</dimen> + <dimen name="default_height_keyline_1">8dp</dimen> + <dimen name="default_height_keyline_2">12dp</dimen> + <dimen name="default_height_keyline_3">16dp</dimen> + <dimen name="default_width_keyline_0">4dp</dimen> + <dimen name="default_width_keyline_1">8dp</dimen> + <dimen name="default_width_keyline_2">8dp</dimen> + <dimen name="default_width_keyline_3">16dp</dimen> + + <dimen name="default_touch_target_minimum_size">68dp</dimen> + <dimen name="default_edge_column_width">@dimen/default_touch_target_minimum_size</dimen> + <dimen name="default_edge_column_margin">@dimen/default_width_keyline_1</dimen> + <dimen name="default_edge_column_width_padded">84dp</dimen> + + <dimen name="default_body1_text_size">32dp</dimen> + <dimen name="default_body1_line_height">40dp</dimen> + <item name="default_body1_letter_spacing" format="float" type="dimen">0.0094</item> + + <dimen name="default_body2_text_size">28dp</dimen> + <dimen name="default_body2_line_height">36dp</dimen> + <item name="default_body2_letter_spacing" format="float" type="dimen">0.0107</item> + + <dimen name="default_body3_text_size">24dp</dimen> + <dimen name="default_body3_line_height">32dp</dimen> + <item name="default_body3_letter_spacing" format="float" type="dimen">0.0250</item> + + <dimen name="default_display1_text_size">56dp</dimen> + <dimen name="default_display1_line_height">64dp</dimen> + <item name="default_display1_letter_spacing" format="float" type="dimen">0.0000</item> + + <dimen name="default_display2_text_size">44dp</dimen> + <dimen name="default_display2_line_height">52dp</dimen> + <item name="default_display2_letter_spacing" format="float" type="dimen">0.0023</item> + + <dimen name="default_display3_text_size">36dp</dimen> + <dimen name="default_display3_line_height">44dp</dimen> + <item name="default_display3_letter_spacing" format="float" type="dimen">0.0055</item> + + <!-- PagedListView --> + <dimen name="default_paged_list_view_max_content_width">704dp</dimen> + + <!-- Focus --> + <dimen name="default_focus_ring_stroke_width_hovered">4dp</dimen> + <dimen name="default_focus_ring_stroke_width_focused">6dp</dimen> + + <!-- Apps Space --> + <dimen name="default_canvas_max_width">794dp</dimen> + <dimen name="default_list_item_corner_radius">8dp</dimen> + <dimen name="default_no_content_focus_background_corner_radius">16dp</dimen> + + <!-- Edit Text --> + <!-- Use a negative padding value to draw the focus ring outside the edit text. --> + <dimen name="default_edit_text_focus_ring_padding">-6dp</dimen> + <dimen name="default_edit_text_focus_ring_radius">12dp</dimen> + +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/floats.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/floats.xml new file mode 100644 index 0000000..970dd93 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/floats.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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> + <!-- GridItems. --> + <!-- Alpha value for the secondary text line in a grid item. --> + <item name="template_grid_item_text_alpha" format="float" type="dimen">0.72</item> + + <!-- Navigation routing template. --> + <!-- Ratio of the image span (width/height) that should be used alongside text. --> + <item name="template_routing_image_span_ratio" format="float" type="dimen">3.0</item> + + <!-- Row list template. --> + <!-- Aspect ratio of the PaneTemplate large image. --> + <item name="template_row_list_large_image_aspect_ratio" format="float" type="dimen">1.75</item> + + <!-- Width ratio of the large image relative to the row list width --> + <item name="template_row_list_to_large_image_ratio" format="float" type="dimen">0.33</item> +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/integers.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/integers.xml new file mode 100644 index 0000000..10a9467 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/integers.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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> + <integer name="template_grid_items_per_row">2</integer> + + <!-- Actions. --> + <!-- + Max ems to display for the button text if there is no icon. + Actual spec is 11ems, but TextView inserts the ellipsize after the maxems + value, so here the value is 11-1 to account for the ellipsize. + --> + <integer name="template_action_button_text_max_ems_no_icon">10</integer> + + <!-- + Max ems to display for the button text if there is an icon. + Actual spec is 10ems, but TextView inserts the ellipsize after the maxems + value, so here the value is 10-1 to account for the ellipsize. + --> + <integer name="template_action_button_text_max_ems_with_icon">9</integer> + + <!-- + Max ems to display for the fab text if there is no icon. + Actual spec is 7ems, but TextView inserts the ellipsize after the maxems + value, so here the value is 7-1 to account for the ellipsize. + --> + <integer name="template_fab_text_max_ems_no_icon">6</integer> + + <!-- + Max ems to display for the fab text if there is an icon. + Actual spec is 6ems, but TextView inserts the ellipsize after the maxems + value, so here the value is 6-1 to account for the ellipsize. + --> + <integer name="template_fab_text_max_ems_with_icon">5</integer> + + <!-- + Max ems to display for the provider sign-in button text. + TODO(b/184195457): remove the custom maxEms limit when we remove the limit for all + --> + <integer name="template_provider_sign_in_button_text_max_ems">100</integer> + + <!-- Duration for the animation of templates switching. --> + <integer name="template_update_animation_duration_millis">300</integer> + + <!-- Layout gravity for content areas (e.g content vertical alignment in Sign In Template + content).--> + <integer name="template_plain_content_layout_gravity">@integer/car_app_ui_content_layout_gravity</integer> + + <!-- Content gravity for content areas (e.g. component horizontal alignment in Sign In + Template content). --> + <integer name="template_plain_content_gravity">@integer/car_app_ui_content_gravity</integer> + + <!-- Gravity integer values (to be used as part of gravity overlayable attributes. --> + <integer name="gravity_bottom">80</integer> + <integer name="gravity_center">17</integer> + <integer name="gravity_center_horizontal">1</integer> + <integer name="gravity_center_vertical">16</integer> + <integer name="gravity_end">8388613</integer> + <integer name="gravity_left">3</integer> + <integer name="gravity_no_gravity">0</integer> + <integer name="gravity_right">5</integer> + <integer name="gravity_start">8388611</integer> + <integer name="gravity_top">48</integer> +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/styles.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/styles.xml new file mode 100644 index 0000000..0779b34 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/styles.xml @@ -0,0 +1,391 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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 xmlns:tools="http://schemas.android.com/tools"> + <!-- Style definitions for the UI in the templates. + + !!! IMPORTANT !!! + Do not refer to these styles directly from views. Styles must be referred + to through theme attributes (in attrs.xml). --> + <style name="Template" parent="Theme.Template"/> + + <!-- The appearance of the markers in the map view. --> + <style name="TextAppearance.Marker" parent="TextAppearance.Template.Body3.Medium"/> + + <style name="MarkerAppearance" parent="TextAppearance.Marker"> + <item name="markerDefaultBackgroundColor">@color/template_marker_default_background_color</item> + <item name="markerCustomBackgroundContentColor">@color/template_marker_custom_background_content_color</item> + <item name="markerDefaultBorderColor">@color/template_marker_default_border_color</item> + <item name="markerCustomBorderColor">@color/template_marker_custom_border_color</item> + <item name="markerPointerWidth">@dimen/template_marker_pointer_width</item> + <item name="markerPointerHeight">@dimen/template_marker_pointer_height</item> + <item name="markerStroke">@dimen/template_marker_stroke</item> + <item name="markerCornerRadius">@dimen/template_marker_corner_radius</item> + <item name="markerPadding">@dimen/template_marker_padding</item> + + <item name="anchorDefaultBackgroundColor">@color/template_anchor_default_background_color</item> + <item name="anchorBorderColor">@color/template_anchor_border_color</item> + <item name="anchorDotColor">@color/template_anchor_dot_color</item> + + <item name="markerTextHorizontalPadding">@dimen/template_marker_text_horizontal_padding</item> + <item name="markerIconSize">@dimen/template_marker_icon_size</item> + <item name="markerImageSize">@dimen/template_marker_image_size</item> + <item name="markerImageCornerRadius">@dimen/template_marker_image_corner_radius</item> + + <item name="markerListIconSize">?templateHalfRowImageSize</item> + </style> + + <style name="MarkerAppearance.Template.Map" parent="MarkerAppearance"> + <item name="markerDefaultContentColor">@color/template_marker_map_default_content_color</item> + </style> + + <style name="MarkerAppearance.Template.List" parent="MarkerAppearance"> + <item name="markerDefaultContentColor">@color/template_marker_list_default_content_color</item> + </style> + + <!-- The style of an action button. --> + <style name="Widget.Template.ActionButton"> + <item name="android:foreground">?templateActionButtonForeground</item> + <item name="android:background">?templateActionButtonBackground</item> + </style> + + <!-- The style of a FAB. --> + <style name="Widget.Template.Fab"> + <item name="android:clickable">true</item> + <item name="android:focusable">true</item> + <item name="android:orientation">horizontal</item> + <item name="android:gravity">center</item> + <item name="android:elevation">6dp</item> + </style> + + <!-- The appearance of a FAB when actually displayed as floating, e.g. + over a map surface. --> + <style name="FabAppearance.Template.Fab" parent="Widget.Template.Fab"> + <item name="android:background">@drawable/action_strip_fab_view_background</item> + <item name="android:foreground">@drawable/action_strip_button_view_focus_ring</item> + <item name="fabDefaultContentColor">@color/template_action_strip_fab_content_color</item> + </style> + + <!-- The appearance of a FAB when displayed in a full screen template. --> + <style name="FabAppearance.Template.FullTemplate" parent="Widget.Template.Fab"> + <item name="android:background">@drawable/action_strip_button_view_background</item> + <item name="android:foreground">@drawable/action_strip_button_view_focus_ring</item> + <item name="fabDefaultContentColor">@color/default_white</item> + </style> + + <!-- The style of a loading spinner. --> + <style name="Widget.Template.Spinner"> + <item name="android:indeterminateDrawable">@drawable/default_progress_spinner</item> + <item name="android:indeterminate">true</item> + </style> + + <!-- The style of a content container. --> + <style name="Widget.Template.Container"> + <item name="android:background">@drawable/car_ui_activity_background</item> + </style> + + <!-- The style of a content container on a surface template. --> + <style name="Widget.Template.Container.Surface"> + <item name="android:background">@android:color/transparent</item> + <item name="android:paddingRight">@dimen/template_plain_content_container_padding</item> + </style> + + <!-- The style of a plain content container. --> + <style name="Widget.Template.Container.Plain" /> + + <!-- The style of a card content container. --> + <style name="Widget.Template.Container.Card"> + <item name="cardBackgroundColor">@color/template_card_background_color</item> + <item name="cardTextColor">@color/template_text_color_primary</item> + <item name="cardFallbackDarkBackgroundColor">@color/default_gray_928</item> + <item name="cardFallbackLightBackgroundColor">@color/template_white</item> + <item name="cardBorderColor">@color/template_card_content_container_border_color</item> + <item name="cardBorderWidth">@dimen/template_card_content_container_border_width</item> + <item name="cardRadius">?templateCornerRadius</item> + <item name="android:elevation">@dimen/template_card_content_container_elevation</item> + </style> + + <!-- The style of a card content container with a content view (e.g. list). --> + <style name="Widget.Template.Container.Card.Content"> + <item name="cardWidthFraction">@dimen/template_card_content_container_width_fraction</item> + <item name="cardMinWidth">@dimen/template_card_content_container_min_width</item> + <item name="cardMaxWidth">@dimen/template_card_content_container_max_width</item> + <item name="cardOemWidth">@dimen/car_app_ui_card_width</item> + <item name="cardOemMaxWidth">@dimen/template_card_content_container_oem_max_width</item> + </style> + + <!-- The style of a card content container with the routing information. --> + <style name="Widget.Template.Container.Card.Content.Routing"> + <item name="cardWidthFraction">@dimen/template_steps_card_content_container_width_fraction</item> + <item name="cardMinWidth">@dimen/template_steps_card_content_container_min_width</item> + <item name="cardMaxWidth">@dimen/template_steps_card_content_container_max_width</item> + <item name="cardOemWidth">@dimen/car_app_ui_nav_card_width</item> + <item name="cardOemMaxWidth">@dimen/template_steps_card_content_container_oem_max_width</item> + </style> + + <!-- The style of a button in a content view. --> + <style name="Widget.Template.ContentButton"> + <item name="android:scaleType">center</item> + <item name="android:tint">@color/template_content_button_color</item> + <item name="android:tintMode">src_atop</item> + </style> + + <!-- The style of a list of rows, plain style. --> + <style name="Widget.Template.RowList"> + <item name="listWidthFraction">@dimen/template_adaptive_width_fraction</item> + <item name="listMaxWidth">@dimen/template_row_list_max_width</item> + <item name="listScrollbarWidth">@dimen/template_paged_list_scrollbar_width</item> + <item name="listShowScrollbarDivider">false</item> + </style> + + <!-- The style of a list of rows, card style. --> + <style name="Widget.Template.RowList.Card"> + <item name="listScrollbarWidth">@dimen/template_paged_list_scrollbar_width_card</item> + <item name="listNoScrollBarStartPadding">@dimen/template_row_list_no_scrollbar_start_padding_card</item> + <!-- Card widths are not adaptive. --> + <item name="listWidthFraction">-1.0</item> + <item name="listShowScrollbarDivider">true</item> + </style> + + <!-- The style of a grid, plain style. --> + <style name="Widget.Template.Grid"> + <item name="listWidthFraction">@dimen/template_adaptive_width_fraction</item> + <item name="listMaxWidth">@dimen/template_grid_max_width</item> + <item name="listScrollbarWidth">@dimen/template_paged_list_scrollbar_width</item> + <item name="listShowScrollbarDivider">false</item> + </style> + + <!-- The style of the title text in a grid item. --> + <style name="Widget.Template.Text.GridItemTitle"> + <item name="android:textAppearance">@style/TextAppearance.CarAppUi.GridItemTitle</item> + <item name="android:gravity">center</item> + </style> + + <!-- The style of the secondary text line below the title in a grid item. --> + <style name="Widget.Template.Text.GridItemText"> + <item name="android:textAppearance">@style/TextAppearance.CarAppUi.GridItemText</item> + <item name="android:gravity">center</item> + </style> + + <!-- The style of text that indicates a grid is empty --> + <style name="Widget.Template.GridEmpty" parent="Widget.CarAppUi.RowSecondary"> + <item name="android:maxLines">2</item> + <item name="android:gravity">center</item> + <!-- TODO(b/174717763): overriding body3's letter spacing since it seems to cause + centered text to be incorrectly cropped --> + <item name="android:letterSpacing">0</item> + </style> + + <style name="Widget.Template.Routing"/> + + <!-- The style of the text showing the title of the routing card when in + arrived state. --> + <style name="Widget.Template.Routing.MessagePrimary"> + <item name="android:textAppearance">@style/TextAppearance.Template.NavCardLargeText</item> + <item name="android:maxLines">2</item> + </style> + + <!-- The style of the text showing the destination address in the routing + card when in arrived state. --> + <style name="Widget.Template.Routing.MessageSecondary"> + <item name="android:textAppearance">@style/TextAppearance.Template.NavCardSmallText</item> + <item name="android:maxLines">2</item> + <item name="android:lineSpacingExtra">5sp</item> + </style> + + <style name="Widget.Template.Routing.Distance"> + <item name="android:textAppearance">@style/TextAppearance.Template.NavCardXLargeText</item> + </style> + + <style name="Widget.Template.Routing.Description"> + <item name="android:textAppearance">@style/TextAppearance.Template.NavCardMediumText</item> + <item name="android:maxLines">2</item> + </style> + + <style name="Widget.Template.Routing.CompactDescription"> + <item name="android:textAppearance">@style/TextAppearance.Template.NavCardSmallText</item> + <item name="android:maxLines">1</item> + </style> + + <style name="Widget.Template.Routing.TravelEstimate"> + <item name="android:textAppearance">@style/TextAppearance.Template.NavCardSmallText</item> + <item name="android:maxLines">1</item> + </style> + + <!-- Routing card textAppearance --> + <style name="TextAppearance.Template.NavCardSmallText" parent="TextAppearance.CarUi.Body3" /> + <style name="TextAppearance.Template.NavCardMediumText" parent="TextAppearance.CarUi.Body2" /> + <style name="TextAppearance.Template.NavCardLargeText" parent="TextAppearance.CarUi.Body1" > + <item name="android:textSize">?templateNavCardLargeTextSize</item> + </style> + <style name="TextAppearance.Template.NavCardXLargeText" parent="TextAppearance.CarUi.Body1"> + <item name="android:textSize">?templateNavCardXLargeTextSize</item> + </style> + + <!-- The style of the title text inside of the message template. --> + <style name="Widget.Template.Text.Message"> + <item name="android:textAppearance">@style/TextAppearance.CarAppUi.TextBlock</item> + <item name="android:maxLines">2</item> + <item name="android:gravity">center</item> + </style> + + <!-- The style of the title text inside of the long message template. --> + <style name="Widget.Template.Text.LongMessage"> + <item name="android:textAppearance">@style/TextAppearance.CarAppUi.TextBlock</item> + <item name="android:maxLines">2147483647</item> + </style> + + <!-- The style of text inside of a debug info inside of the message template. --> + <style name="Widget.Template.Debug" parent="Widget.Template.Text.Body3"> + <item name="android:maxLines">1024</item> + <item name="android:textColor">@color/template_message_debug_text_color</item> + <item name="android:fontFamily">monospace</item> + <item name="android:textSize">@dimen/template_message_debug_text_size</item> + <item name="android:lineHeight" tools:ignore="NewApi">@dimen/template_message_debug_text_line_height</item> + </style> + + <style name="Widget.Template.SignIn"/> + + <!-- The style of the container of sign-in content. --> + <style name="Widget.Template.SignIn.Container"> + <item name="android:gravity">?templatePlainContentGravity</item> + <item name="android:layout_gravity">?templatePlainContentLayoutGravity</item> + <item name="android:layout_marginHorizontal">?templatePlainContentHorizontalPadding</item> + </style> + + <!-- The style of the instruction text inside the sign-in template. --> + <style name="Widget.Template.SignIn.InstructionText"> + <item name="android:textAppearance">@style/TextAppearance.CarAppUi.SignInHeader</item> + <item name="android:maxLines">2</item> + <item name="android:gravity">center</item> + </style> + + <!-- The style of a provider sign-in button. --> + <style name="Widget.Template.SignIn.ProviderSignInbutton" parent="Widget.Template.ActionButton"> + <item name="textMaxEms">@integer/template_provider_sign_in_button_text_max_ems</item> + </style> + + <!-- The style of the PIN text inside the sign-in template. --> + <style name="Widget.Template.SignIn.PinText"> + <item name="android:textAppearance">@style/TextAppearance.CarAppUi.ReadOnlyText</item> + <item name="android:maxLines">1</item> + <item name="android:gravity">center</item> + <item name="android:letterSpacing">@dimen/template_sign_in_pin_text_letter_spacing</item> + </style> + + <!-- The style of the additional text inside the sign-in template. --> + <style name="Widget.Template.SignIn.AdditionalText"> + <item name="android:textAppearance">@style/TextAppearance.CarAppUi.SignInLegal</item> + <item name="android:maxLines">3</item> + <item name="android:gravity">center</item> + <item name="android:textColor">@color/template_sign_in_additional_text_color</item> + <item name="android:textColorLink">?templateHyperlinkTextColor</item> + <item name="android:linksClickable">true</item> + </style> + + <!-- The style of the error message inside the sign-in template. --> + <style name="Widget.Template.SignIn.ErrorMessage" parent="Widget.Template.Text.Body3"> + <item name="android:maxLines">1</item> + <item name="android:gravity">start</item> + <item name="android:textColor">@color/template_sign_in_error_message_color</item> + <item name="android:textSize">@dimen/template_sign_in_input_error_message_text_size</item> + <item name="android:layout_marginStart">?templateEditTextErrorHorizontalSpacing</item> + <item name="android:layout_marginTop">?templateEditTextErrorVerticalSpacing</item> + </style> + + <!-- The style of the text in an action button. --> + <style name="Widget.Template.Text.ActionButton"> + <item name="android:textAppearance">@style/TextAppearance.CarAppUi.ButtonText</item> + <item name="android:textColor">@color/car_app_ui_action_button_text_color</item> + </style> + + <!-- Standard styles for all template widgets. + + These styles can be referenced directly from widgets without using a + theme attribute. + + See go/watevra-visd. --> + <style name="TextAppearance.Template" parent="TextAppearance.Design"/> + + <!-- Styles for display text, meant for larger text like titles and such. --> + <style name="TextAppearance.Template.Display1" parent="TextAppearance.Design.Display1"/> + <style name="TextAppearance.Template.Display2" parent="TextAppearance.Design.Display2"/> + <style name="TextAppearance.Template.Display2.Medium" parent="TextAppearance.Design.Display2.Medium"/> + <style name="TextAppearance.Template.Display3" parent="TextAppearance.Design.Display3"/> + + <!-- Styles for body text, meant for smaller text like list row contents. --> + <style name="TextAppearance.Template.Body1" parent="TextAppearance.Design.Body1"/> + <style name="TextAppearance.Template.Body2" parent="TextAppearance.Design.Body2"/> + <style name="TextAppearance.Template.Body3" parent="TextAppearance.Design.Body3"/> + <style name="TextAppearance.Template.Body3.Medium" parent="TextAppearance.Design.Body3.Medium"/> + + <style name="Widget"/> + <style name="Widget.Template"/> + + <!-- Base style for text widgets. --> + <style name="Widget.Template.Text"> + <item name="android:maxLines">1</item> + <item name="android:ellipsize">end</item> + </style> + + <!-- Styles for text widgets. There's one per typographic style as declare in + the specification: + https://designguidelines.withgoogle.com/automotive-os-apps/visual-foundations/typography.html#typography-scale-grid-references + + Note certain paragraph-level attributes such as `lineHeight` can't be + folded into the `TextAppearance` hence why we need to declare these as + part of a style. --> + <style name="Widget.Template.Text.Display1"> + <item name="android:textAppearance">@style/TextAppearance.Template.Display1</item> + <item name="android:lineHeight" tools:ignore="NewApi">@dimen/default_display1_line_height</item> + <item name="android:letterSpacing">@dimen/default_display1_letter_spacing</item> + </style> + + <style name="Widget.Template.Text.Display2"> + <item name="android:textAppearance">@style/TextAppearance.Template.Display2</item> + <item name="android:lineHeight" tools:ignore="NewApi">@dimen/default_display2_line_height</item> + <item name="android:letterSpacing">@dimen/default_display2_letter_spacing</item> + </style> + + <style name="Widget.Template.Text.Display3"> + <item name="android:textAppearance">@style/TextAppearance.Template.Display3</item> + <item name="android:lineHeight" tools:ignore="NewApi">@dimen/default_display3_line_height</item> + <item name="android:letterSpacing">@dimen/default_display3_letter_spacing</item> + </style> + + <style name="Widget.Template.Text.Body1"> + <item name="android:textAppearance">@style/TextAppearance.Template.Body1</item> + <item name="android:lineHeight" tools:ignore="NewApi">@dimen/default_body1_line_height</item> + <item name="android:letterSpacing">@dimen/default_body1_letter_spacing</item> + </style> + + <style name="Widget.Template.Text.Body2"> + <item name="android:textAppearance">@style/TextAppearance.Template.Body2</item> + <item name="android:lineHeight" tools:ignore="NewApi">@dimen/default_body2_line_height</item> + <item name="android:letterSpacing">@dimen/default_body2_letter_spacing</item> + </style> + + <style name="Widget.Template.Text.Body3"> + <item name="android:textAppearance">@style/TextAppearance.Template.Body3</item> + <item name="android:lineHeight" tools:ignore="NewApi">@dimen/default_body3_line_height</item> + <item name="android:letterSpacing">@dimen/default_body3_letter_spacing</item> + </style> + + <style name="Widget.Template.Text.Body3.Medium"> + <item name="android:textAppearance">@style/TextAppearance.Template.Body3.Medium</item> + </style> +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/styles_default.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/styles_default.xml new file mode 100644 index 0000000..3595ae0 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/styles_default.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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 xmlns:tools="http://schemas.android.com/tools"> + + <!-- Typography spec: + https://designguidelines.withgoogle.com/automotive-os-apps/visual-foundations/typography.html#typography-scale-grid-references --> + <style name="TextAppearance.Design" parent="TextAppearance.CarUi"> + <item name="android:textStyle">normal</item> + <item name="android:textColor">@color/default_text_color</item> + </style> + + <style name="TextAppearance.Design.Body1" parent="TextAppearance.CarUi.Body1"/> + + <style name="TextAppearance.Design.Body2" parent="TextAppearance.CarUi.Body2"/> + + <style name="TextAppearance.Design.Body3" parent="TextAppearance.CarUi.Body3"/> + + <style name="TextAppearance.Design.Body3.Medium" parent="TextAppearance.Design.Body3"> + <item name="android:fontFamily">sans-serif-medium</item> + </style> + + <style name="TextAppearance.Design.Display1" parent="TextAppearance.CarUi.Body1"> + <item name="android:textSize">@dimen/default_display1_text_size</item> + </style> + + <style name="TextAppearance.Design.Display2" parent="TextAppearance.CarUi.Body1"> + <item name="android:textSize">@dimen/default_display2_text_size</item> + </style> + + <style name="TextAppearance.Design.Display2.Medium" parent="TextAppearance.Design.Display2"> + <item name="android:fontFamily">sans-serif-medium</item> + </style> + + <style name="TextAppearance.Design.Display3" parent="TextAppearance.CarUi.Body1"> + <item name="android:textSize">@dimen/default_display3_text_size</item> + </style> + +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/themes.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/themes.xml new file mode 100644 index 0000000..16ae280 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/themes.xml @@ -0,0 +1,344 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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> + <!-- The template app theme. + + <p>This theme must be set on the root template view which is the parent of all the + template layouts and their child views. + + <p>Styles used by this theme are named using the convention Type.Template.Etc + (for example {@code TextAppearance.Template.RowTitle}).--> + + <style name="Theme.Template" parent="Theme.CarUi"> + + <!-- Common. --> + <item name="templateAdaptiveWidthFraction">@dimen/template_adaptive_width_fraction</item> + <!-- Margin between buttons placed side by side. --> + <item name="templateControlToControlSpacingHorizontal">@dimen/car_app_ui_control_to_control_spacing_horizontal</item> + <!-- Button height. --> + <item name="templateButtonHeight">@dimen/car_app_ui_button_height</item> + <!-- Corner radius used across the UI except for the buttons. --> + <item name="templateCornerRadius">@dimen/car_app_ui_corner_radius</item> + <!-- Corner radius used for the buttons. --> + <item name="templateButtonCornerRadius">@dimen/car_app_ui_button_corner_radius</item> + + <!-- Standard colors. --> + <item name="templateStandardBlue">@color/car_app_ui_standard_blue</item> + <item name="templateStandardRed">@color/car_app_ui_standard_red</item> + <item name="templateStandardGreen">@color/car_app_ui_standard_green</item> + <item name="templateStandardYellow">@color/car_app_ui_standard_yellow</item> + + <!-- Images. --> + <item name="templateLargeImageSize">@dimen/template_large_image_size</item> + <item name="templateLargeImageSizeMin">@dimen/template_large_image_size_min</item> + <item name="templateLargeImageSizeMax">@dimen/template_large_image_size_max</item> + + <!-- Sign In Template. --> + <item name="templateSignInContainerStyle">@style/Widget.Template.SignIn.Container</item> + <item name="templateSignInMethodViewMaxWidth">@dimen/template_sign_in_method_view_max_width</item> + <item name="templateSignInInstructionTextStyle">@style/Widget.Template.SignIn.InstructionText</item> + <item name="templateSignInProviderSignInButtonStyle">@style/Widget.Template.SignIn.ProviderSignInbutton</item> + <item name="templateSignInPinTextStyle">@style/Widget.Template.SignIn.PinText</item> + <item name="templateSignInPinBackgroundColor">@color/template_read_only_text_background_color</item> + <item name="templateSignInPinCornerRadius">?templateCornerRadius</item> + <item name="templateSignInPinBackground">@drawable/pin_sign_in_view_background</item> + <item name="templateSignInPinPadding">@dimen/template_read_only_text_padding</item> + <item name="templateSignInQRCodeImageWidth">@dimen/template_sign_in_qr_code_image_width</item> + <item name="templateSignInAdditionalTextStyle">@style/Widget.Template.SignIn.AdditionalText</item> + <item name="templateSignInErrorMessageStyle">@style/Widget.Template.SignIn.ErrorMessage</item> + <item name="templateSignInInputViewStyle">@style/Widget.CarAppUi.InputView</item> + + <!-- Hyperlink Text --> + <item name="templateHyperlinkTextColor">@color/car_app_ui_hyperlink_text_color</item> + + <!-- Map markers. --> + <item name="templateMapMarkerAppearance">@style/MarkerAppearance.Template.Map</item> + <item name="templateListMarkerAppearance">@style/MarkerAppearance.Template.List</item> + + <!-- Loading spinners. --> + <item name="templateLoadingSpinnerStyle">@style/Widget.Template.Spinner</item> + <item name="templateLoadingSpinnerColor">@color/template_loading_spinner_color</item> + + <!-- Plain content containers. --> + <item name="templatePlainContentContainerStyle">@style/Widget.Template.Container.Plain</item> + <item name="templatePlainContentContainerWidth">@dimen/template_plain_content_container_width</item> + <item name="templatePlainContentLayoutGravity">@integer/template_plain_content_layout_gravity</item> + <item name="templatePlainContentGravity">@integer/template_plain_content_gravity</item> + <item name="templatePlainContentHorizontalPadding">@dimen/template_plain_content_horizontal_padding</item> + <item name="templatePlainContentBackgroundColor">@color/template_plain_content_background_color</item> + + <!-- Card content containers. --> + <item name="templateCardContentContainerStyle">@style/Widget.Template.Container.Card.Content</item> + <item name="templateCardRoutingContentContainerStyle">@style/Widget.Template.Container.Card.Content.Routing</item> + <item name="templateCardContentContainerDefaultWidth">@dimen/template_card_content_container_default_width</item> + <item name="templateCardContentContainerStartMargin">@dimen/template_card_content_container_start_margin</item> + <item name="templateCardContentContainerTopMargin">@dimen/template_card_content_container_top_margin</item> + <item name="templateCardContentContainerBottomMargin">@dimen/template_card_content_container_bottom_margin</item> + <item name="templateCardContentContainerMinHeight">@dimen/template_card_content_container_min_height</item> + + <!-- Content headers. --> + <item name="templateHeaderHeight">@dimen/template_header_height</item> + <item name="templateHeaderTextVerticalSpacing">@dimen/template_header_text_vertical_spacing</item> + <item name="templateHeaderTextStartSpacing">@dimen/template_header_text_horizontal_spacing</item> + <item name="templateHeaderTextEndSpacing">@dimen/template_header_text_horizontal_spacing</item> + <item name="templateHeaderTextStyle">@style/TextAppearance.CarAppUi.CardHeader</item> + <item name="templateHeaderTextNoIconStartSpacing">@dimen/template_header_text_no_icon_start_spacing</item> + <item name="templateHeaderButtonIconSize">@dimen/template_header_button_icon_size</item> + <item name="templateHeaderButtonIconTint">@color/template_icon_tint_color</item> + <item name="templateHeaderButtonContainerSize">@dimen/template_button_touch_target_size</item> + <item name="templateHeaderButtonStartSpacing">@dimen/template_header_button_start_spacing</item> + <item name="templateHeaderButtonBackground">@drawable/template_header_button_background</item> + <item name="templateHeaderBackgroundColor">@color/template_container_background</item> + + <!-- Dividers. --> + <item name="templateDividerThickness">@dimen/template_divider_thickness</item> + <item name="templateDividerColor">@color/default_gradient_white_24</item> + + <!-- Rows. --> + <item name="templateRippleColor">@color/template_ripple_color</item> + <item name="templateRippleSelectorColor">@color/template_ripple_selector_color</item> + <item name="templateRowBackgroundColor">@color/car_app_ui_row_background_color</item> + <item name="templateRowSelectedBackgroundColor">@color/default_gray_878</item> + <item name="templateRowImagePlaceholderColor">@color/default_gray_928</item> + <item name="templateRowBackgroundSimple">@drawable/row_background_simple</item> + <item name="templateRowBackgroundSectionalMiddle">@drawable/row_background_sectional_middle</item> + <item name="templateRowBackgroundSectionalTop">@drawable/row_background_sectional_top</item> + <item name="templateRowBackgroundSectionalBottom">@drawable/row_background_sectional_bottom</item> + <item name="templateRowBackgroundSectionalTopBottom">@drawable/row_background_sectional_top_bottom</item> + <item name="templateRowBackgroundSectionalBottomMargin">@dimen/template_row_background_sectional_bottom_margin</item> + <item name="templateRowImagePlaceholder">@drawable/row_image_placeholder</item> + <item name="templateRowMinHeight">@dimen/template_row_min_height</item> + <item name="templateRowIconSize">@dimen/template_row_icon_size</item> + <item name="templateRowImageSizeSmall">@dimen/template_row_image_size_small</item> + <item name="templateRowImageSizeLarge">@dimen/template_row_image_size_large</item> + <item name="templateRowMarkerMinSize">@dimen/template_row_marker_min_size</item> + <item name="templateRowMarkerLabelMargin">@dimen/template_row_marker_label_margin</item> + <item name="templateRowDefaultIconTint">@color/template_icon_tint_color</item> + <item name="templateRowHorizontalPadding">@dimen/template_row_horizontal_padding</item> + <item name="templateRowHorizontalHalfPadding">@dimen/template_row_horizontal_half_padding</item> + <item name="templateRowTextHorizontalPadding">@dimen/template_row_text_horizontal_padding</item> + <item name="templateRowTextHorizontalHalfPadding">@dimen/template_row_text_horizontal_half_padding</item> + <item name="templateHalfListBottomPadding">@dimen/template_half_list_bottom_padding</item> + <item name="templateHalfListPaddingVertical">@dimen/template_half_row_padding_vertical</item> + + <!-- Half rows. --> + <item name="templateHalfRowMinHeight">@dimen/car_app_ui_half_row_min_height</item> + <item name="templateHalfRowHorizontalPadding">@dimen/template_half_row_horizontal_padding</item> + <item name="templateHalfRowVerticalPadding">@dimen/template_half_row_vertical_padding</item> + <item name="templateHalfRowImageToTextSpacing">@dimen/template_half_row_image_to_text_margin</item> + <item name="templateHalfRowTextToTextSpacing">@dimen/template_half_row_text_to_text_margin</item> + <item name="templateHalfRowImageSize">@dimen/template_half_row_image_size</item> + + <!-- Full rows. --> + <item name="templateFullRowStartPadding">@dimen/template_full_row_start_padding</item> + <item name="templateFullRowEndPadding">@dimen/template_full_row_end_padding</item> + <item name="templateFullRowChevronIcon">@drawable/car_ui_preference_icon_chevron</item> + <item name="templateHalfRowChevronIcon">@drawable/car_ui_preference_icon_chevron</item> + <item name="templateFullRowChevronHeight">@dimen/template_full_row_chevron_size</item> + <item name="templateFullRowChevronWidth">@dimen/template_full_row_chevron_size</item> + + <!-- Note, these containers are sized to match the height of the title of the row + so that they appear vertically aligned at the center when both are top aligned. --> + <item name="templateRowSelectionContainerHeight">@dimen/default_body2_line_height</item> + + <!-- Text styles for list elements. --> + <item name="templateRowTitleStyle">@style/Widget.CarAppUi.RowTitle</item> + <item name="templateRowSecondaryTextStyle">@style/Widget.CarAppUi.RowSecondary</item> + <item name="templateRowSectionHeaderStyle">@style/Widget.CarAppUi.RowSectionHeader</item> + <item name="templateRowListEmptyTextStyle">@style/Widget.CarAppUi.RowListEmpty</item> + + <!-- The image dimensions (for PaneTemplate) in the row list template. --> + <item name="templateRowListToLargeImageRatio">@dimen/template_row_list_to_large_image_ratio</item> + <item name="templateRowListLargeImageContainerMaxWidth">@dimen/template_row_list_large_image_container_max_width</item> + <item name="templateRowListLargeImageAspectRatio">@dimen/template_row_list_large_image_aspect_ratio</item> + + <!-- Padding between the (PaneTemplate) image and row list --> + <item name="templateRowListAndImagePadding">@dimen/default_width_keyline_1</item> + + <!-- Grids. --> + <item name="templateGridStyle">@style/Widget.Template.Grid</item> + + <!-- Grid items. --> + <item name="templateGridItemImageBottomPadding">@dimen/template_grid_item_image_bottom_padding</item> + <item name="templateGridItemDefaultIconTint">@color/template_icon_tint_color</item> + <item name="templateGridItemTextContainerMaxWidth">@dimen/template_grid_item_text_container_max_width</item> + <item name="templateGridItemTextBottomPadding">@dimen/template_grid_item_text_bottom_padding</item> + <item name="templateGridItemHorizontalSpacing">@dimen/template_grid_item_horizontal_spacing</item> + <item name="templateGridItemVerticalSpacing">@dimen/template_grid_item_vertical_spacing</item> + <item name="templateGridItemsPerRow">@integer/template_grid_items_per_row</item> + <item name="templateGridEmptyTextStyle">@style/Widget.Template.GridEmpty</item> + <item name="templateGridItemBackground">@drawable/template_grid_item_background</item> + <item name="templateGridItemBackgroundColor">@color/car_app_ui_grid_item_background_color</item> + + <!-- Text styles for grid items. --> + <item name="templateGridItemTitleStyle">@style/Widget.Template.Text.GridItemTitle</item> + <item name="templateGridItemTextStyle">@style/Widget.Template.Text.GridItemText</item> + + <!-- Action buttons and FABs. --> + <item name="templateActionButtonMargin">?templateControlToControlSpacingHorizontal</item> + <item name="templateActionButtonStyle">@style/Widget.Template.ActionButton</item> + <item name="templateActionButtonTextStyle">@style/Widget.Template.Text.ActionButton</item> + <item name="templateActionButtonForeground">@drawable/action_button_focus_ring</item> + <item name="templateActionButtonDefaultBackgroundColor">@color/car_app_ui_action_button_default_background_color</item> + <item name="templateActionButtonPrimaryBackgroundColor">@color/car_app_ui_action_button_primary_background_color</item> + <item name="templateActionButtonBackground">@drawable/car_app_ui_action_button_background</item> + <item name="templateActionButtonHeight">?templateButtonHeight</item> + <item name="templateActionButtonTouchTargetSize">@dimen/template_button_touch_target_size</item> + <item name="templateActionButtonListButtonStretchHorizontal">@bool/car_app_ui_action_button_list_button_stretch_horizontal</item> + <item name="templateActionButtonListGravity">@integer/car_app_ui_action_button_list_gravity</item> + <item name="templateActionButtonListButtonContentAlignment">@integer/car_app_ui_action_button_list_button_content_alignment</item> + <item name="templateActionButtonListButtonMaxWidth">@dimen/car_app_ui_action_button_list_button_max_width</item> + <item name="templateActionButtonSideAlignmentSpacing">@dimen/car_app_ui_button_side_alignment_spacing</item> + <item name="templateActionButtonListRowVerticalSpacing">@dimen/template_action_button_list_row_vertical_spacing</item> + <item name="templateActionIconSize">@dimen/template_action_icon_size</item> + <item name="templateActionIconSizeMin">@dimen/template_action_icon_size_min</item> + <item name="templateActionIconSizeMax">@dimen/template_action_icon_size_max</item> + <item name="templateActionIconTextStartSpacing">@dimen/template_action_icon_text_start_spacing</item> + <item name="templateActionIconTextEndSpacing">@dimen/template_action_icon_text_end_spacing</item> + <item name="templateActionIconToTextSpacing">@dimen/template_action_icon_to_text_spacing</item> + <item name="templateActionTextHorizontalSpacing">@dimen/template_action_text_horizontal_spacing</item> + <item name="templateActionWithTextMinWidth">@dimen/template_action_with_text_min_width</item> + <item name="templateActionButtonUseOemColors">@bool/car_app_ui_is_action_color_overridden</item> + <item name="templateActionButtonPrimaryHorizontalOrder">@integer/car_app_ui_action_button_primary_horizontal_order</item> + <!-- Min width common to buttons and FABs. It is the same as the height of + the action button so that the button appears with 1:1 aspect ratio when it has just + an icon inside. --> + <item name="templateActionWithoutTextMinWidth">?templateButtonHeight</item> + <item name="templateActionDefaultIconTint">@color/template_white</item> + <item name="templateActionButtonTextMaxEmsNoIcon">@integer/template_action_button_text_max_ems_no_icon</item> + <item name="templateActionButtonTextMaxEmsWithIcon">@integer/template_action_button_text_max_ems_with_icon</item> + <item name="templateFabTextMaxEmsNoIcon">@integer/template_fab_text_max_ems_no_icon</item> + <item name="templateFabTextMaxEmsWithIcon">@integer/template_fab_text_max_ems_with_icon</item> + <item name="templateActionButtonSecondaryBorderWidth">@dimen/template_action_button_secondary_border_width</item> + <item name="templateActionButtonSecondaryBorderColor">@color/template_white_46</item> + <item name="templateActionStripButtonMargin">?templateControlToControlSpacingHorizontal</item> + <item name="templateActionStripPadding">@dimen/template_action_strip_padding</item> + + <item name="templateActionStripButtonBackgroundColor">@color/template_black</item> + <item name="templateActionStripFabAppearance">@style/FabAppearance.Template.Fab</item> + <item name="templateActionStripFullTemplateFabAppearance">@style/FabAppearance.Template.FullTemplate</item> + + <!-- FAB background color. + They point to the same resource, which is set to a different value between light and dark modes. --> + <item name="templateActionStripFabBackgroundColorLight">@color/template_action_strip_fab_background_color</item> + <item name="templateActionStripFabBackgroundColorDark">@color/template_action_strip_fab_background_color</item> + + <!-- Toggles and radio buttons. --> + <item name="templateToggleWidth">@dimen/template_toggle_width</item> + <item name="templateToggleHeight">@dimen/template_toggle_height</item> + <item name="templateToggleInactiveTrackColor">@color/template_toggle_inactive_track</item> + <item name="templateToggleInactiveThumbColor">@color/template_toggle_inactive_thumb</item> + <item name="templateToggleActiveTrackColor">@color/template_toggle_active_track</item> + <item name="templateToggleActiveThumbColor">@color/template_toggle_active_thumb</item> + <item name="templateRadioButtonSize">@dimen/template_radio_button_size</item> + + <!-- Clickable spans. --> + <item name="templateClickableSpanHighlightForegroundColor">@color/template_black</item> + <item name="templateClickableSpanHighlightBackgroundColor">@color/template_focus_ring_color_selector</item> + + <!-- Full screen message --> + <item name="templateMessageDefaultIconTint">@color/template_white</item> + <item name="templateMessageTitleTextStyle">@style/Widget.Template.Text.Message</item> + <item name="templateMessageTitleTopSpacing">@dimen/template_message_title_top_spacing</item> + <item name="templateMessageButtonsTopSpacing">@dimen/template_message_buttons_top_spacing</item> + <item name="templateStickyButtonsVerticalSpacing">@dimen/template_sticky_buttons_vertical_spacing</item> + + <item name="templateMessageLongTextStyle">@style/Widget.Template.Text.LongMessage</item> + <item name="templateMessageDebugTextStyle">@style/Widget.Template.Debug</item> + <item name="templateDebugMessageBackgroundColor">@color/template_gray_900</item> + + <!-- Focus. --> + <item name="templateFocusAccentColor">@color/template_focus_ring_color_selector</item> + <item name="templateFocusNoContentAccentColor">@color/default_focus_no_content</item> + <item name="templateFocusRingColor">@color/default_focus_blue</item> + <item name="templateFocusRingNoAccentColor">@color/default_focus_no_content</item> + + <!-- EditText. --> + <item name="templateEditTextStyle">@style/Widget.CarAppUi.EditText</item> + <item name="templateEditTextActiveColor">@color/template_edit_text_active_color</item> + <item name="templateEditTextEnabledColor">@color/template_edit_text_enabled_color</item> + <item name="templateEditTextErrorColor">@color/template_edit_text_error_color</item> + <item name="templateEditTextDisabledColor">@color/template_edit_text_disabled_color</item> + <item name="templateEditTextErrorVerticalSpacing">@dimen/car_app_ui_edit_text_error_vertical_spacing</item> + <item name="templateEditTextErrorHorizontalSpacing">@dimen/car_app_ui_edit_text_error_horizontal_spacing</item> + + <!-- Search bar. --> + <item name="templateSearchBarMaxWidth">@dimen/template_search_bar_max_width</item> + <item name="templateSearchBarIcon">@drawable/search_bar_icon</item> + + <!-- Routing --> + <item name="templateRoutingStepsCardIconToDistanceSpacingHorizontal">@dimen/template_steps_card_image_to_text_spacing_horizontal</item> + <item name="templateRoutingImageSpanRatio">@dimen/template_routing_image_span_ratio</item> + <item name="templateRoutingImageSpanBody2MaxHeight">@dimen/template_routing_image_span_body2_max_height</item> + <item name="templateRoutingImageSpanBody3MaxHeight">@dimen/template_routing_image_span_body3_max_height</item> + <item name="templateNavCardLargeTextSize" format="dimension">@dimen/car_app_ui_nav_card_large_text_size</item> + <item name="templateNavCardXLargeTextSize" format="dimension">@dimen/car_app_ui_nav_card_xlarge_text_size</item> + <item name="templateNavCardLargeImageSize" format="dimension">@dimen/template_nav_card_large_image_size</item> + <item name="templateNavCardLargeImageSizeMin" format="dimension">@dimen/template_nav_card_large_image_size_min</item> + <item name="templateNavCardLargeImageSizeMax" format="dimension">@dimen/template_nav_card_large_image_size_max</item> + <item name="templateNavCardSmallImageSize" format="dimension">@dimen/template_nav_card_small_image_size</item> + <item name="templateNavCardSmallImageSizeMin" format="dimension">@dimen/template_nav_card_small_image_size_min</item> + <item name="templateNavCardSmallImageSizeMax" format="dimension">@dimen/template_nav_card_small_image_size_max</item> + <item name="templateNavCardFallbackContentColor">@color/default_white</item> + <item name="templateRoutingDistanceStyle">@style/Widget.Template.Routing.Distance</item> + <item name="templateRoutingDescriptionStyle">@style/Widget.Template.Routing.Description</item> + <item name="templateRoutingCompactDescriptionStyle">@style/Widget.Template.Routing.CompactDescription</item> + <item name="templateRoutingTravelEstimateStyle">@style/Widget.Template.Routing.TravelEstimate</item> + <item name="templateRoutingLanesImageContainerHeight">@dimen/template_routing_lanes_image_container_height</item> + <item name="templateRoutingLanesImageContainerVerticalPadding">@dimen/template_padding_0</item> + <item name="templateRoutingLanesImageContainerHorizontalPadding">@dimen/template_padding_2</item> + <item name="templateRoutingLanesImageBackgroundColor">@color/template_white_16</item> + <item name="templateRoutingJunctionImageBackgroundColor">@color/template_white_16</item> + <item name="templateRoutingMessagePrimaryStyle">@style/Widget.Template.Routing.MessagePrimary</item> + <item name="templateRoutingMessageSecondaryStyle">@style/Widget.Template.Routing.MessageSecondary</item> + <item name="templateRoutingMessageInnerPaddingHorizontal">@dimen/template_padding_3</item> + <item name="templateRoutingMessageInnerPaddingVertical">@dimen/template_padding_1</item> + <item name="templateNavCardPaddingHorizontal">@dimen/template_nav_card_padding_horizontal</item> + <item name="templateNavCardPaddingVertical">@dimen/template_nav_card_padding_vertical</item> + <item name="templateNavCardSmallPaddingVertical">@dimen/template_nav_card_small_padding_vertical</item> + <item name="templateRoutingStepsCardContentContainerMinWidth">@dimen/template_steps_card_content_container_min_width</item> + <item name="templateRoutingStepsCardContentContainerMinHeight">@dimen/template_steps_card_content_container_min_height</item> + <item name="templateRoutingDividerColor">@color/template_white_16</item> + + <!-- Status bar gradient background start and end colors. --> + <item name="templateStatusBarStartColor">@android:color/transparent</item> + <item name="templateStatusBarEndColor">@color/template_status_bar_end_color</item> + <item name="templateStatusBarMinimumTopPadding">@dimen/template_status_bar_minimum_top_padding</item> + + <!-- No content view. --> + <item name="templateNoContentFocusCornerRadius">@dimen/template_no_content_view_focus_corner_radius</item> + + <!-- Animation. --> + <item name="templateUpdateAnimationDurationMilliseconds">@integer/template_update_animation_duration_millis</item> + + <!-- This is necessary so that the floating elements such as the action + strip don't get their shadows clipped. --> + <item name="android:clipChildren">false</item> + <item name="android:clipToPadding">false</item> + + <!-- The max number of rows in a list view. --> + <item name="templateListMaxLength">@integer/car_app_ui_list_max_length</item> + + <!-- The max number of grid items in a grid view. --> + <item name="templateGridMaxLength">@integer/car_app_ui_grid_max_length</item> + + <item name="templateSendNavStateToSystem">@bool/send_navstates_to_system</item> + + </style> + +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/AbstractHeaderView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/AbstractHeaderView.java new file mode 100644 index 0000000..9babeca --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/AbstractHeaderView.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import android.graphics.drawable.Drawable; +import android.util.Log; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.car.app.model.Action; +import androidx.car.app.model.ActionStrip; +import androidx.car.app.model.CarColor; +import androidx.car.app.model.CarIcon; +import androidx.car.app.model.OnClickDelegate; +import com.android.car.libraries.apphost.common.CarAppError; +import com.android.car.libraries.apphost.common.CommonUtils; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.template.view.model.ActionStripWrapper; +import com.android.car.libraries.apphost.view.common.CarTextUtils; +import com.android.car.libraries.apphost.view.common.ImageUtils; +import com.android.car.libraries.apphost.view.common.ImageViewParams; +import com.android.car.ui.toolbar.MenuItem; +import com.android.car.ui.toolbar.Toolbar; +import com.android.car.ui.toolbar.Toolbar.NavButtonMode; +import com.android.car.ui.toolbar.ToolbarController; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** A view that displays the header for the templates. */ +public abstract class AbstractHeaderView { + protected final TemplateContext mTemplateContext; + protected final ToolbarController mToolbarController; + + // TODO(b/183853224): Replace with equivalent ToolbarController, once is available + @SuppressWarnings("deprecation") + private final Toolbar.OnBackListener mBackListener = + new Toolbar.OnBackListener() { + @Override + public boolean onBack() { + if (mTemplateContext != null) { + mTemplateContext.getBackPressedHandler().onBackPressed(); + } + return true; + } + }; + + protected AbstractHeaderView( + TemplateContext templateContext, ToolbarController toolbarController) { + mTemplateContext = templateContext; + mToolbarController = toolbarController; + } + + @VisibleForTesting + protected ToolbarController getToolbarController() { + return mToolbarController; + } + + /** Updates the header action */ + protected void setAction(@Nullable Action action) { + if (action != null && action.getType() == Action.TYPE_BACK) { + mToolbarController.registerOnBackListener(mBackListener); + mToolbarController.setNavButtonMode(NavButtonMode.BACK); + } else { + mToolbarController.unregisterOnBackListener(mBackListener); + mToolbarController.setNavButtonMode(NavButtonMode.DISABLED); + } + + if (action != null && action.getType() == Action.TYPE_APP_ICON) { + mToolbarController.setLogo(mTemplateContext.getCarAppPackageInfo().getRoundAppIcon()); + } else { + mToolbarController.setLogo(0); + } + } + + /** Updates the [ActionStrip] associated with this toolbar */ + public void setActionStrip(@Nullable ActionStrip actionStrip, ActionsConstraints constraints) { + validateActionStrip(actionStrip, constraints); + if (actionStrip == null) { + mToolbarController.setMenuItems(null); + } else { + List<MenuItem> menuItems = createMenuItems(actionStrip, mTemplateContext); + mToolbarController.setMenuItems(menuItems); + } + } + + /** Adds a toggle to this toolbar */ + public void addToggle(@Nullable Drawable icon, @Nullable Consumer<Boolean> onClickListener) { + List<MenuItem> menuItemList = new ArrayList<>(mToolbarController.getMenuItems()); + if (icon != null) { + menuItemList.add(new MenuItem.Builder(mTemplateContext).setIcon(icon).build()); + } + menuItemList.add( + new MenuItem.Builder(mTemplateContext) + .setCheckable() + .setOnClickListener( + item -> { + if (onClickListener != null) { + onClickListener.accept(item.isChecked()); + } + }) + .build()); + mToolbarController.setMenuItems(menuItemList); + } + + /** Ensure the model satisfies the input constraints. */ + private void validateActionStrip( + @Nullable ActionStrip actionStrip, ActionsConstraints constraints) { + ActionStripWrapper actionStripWrapper = + actionStrip == null ? null : new ActionStripWrapper.Builder(actionStrip).build(); + try { + ActionStripUtils.validateRequiredTypes(actionStripWrapper, constraints); + } catch (ActionStripUtils.ValidationException exception) { + mTemplateContext + .getErrorHandler() + .showError( + CarAppError.builder(mTemplateContext.getCarAppPackageInfo().getComponentName()) + .setCause(exception) + .build()); + } + } + + /** Converts an [ActionStrip] to a list of [MenuItem]s. */ + protected static List<MenuItem> createMenuItems( + ActionStrip actionStrip, TemplateContext templateContext) { + List<MenuItem> menuItems = new ArrayList<>(); + for (Object action : actionStrip.getActions()) { + if (action instanceof Action) { + MenuItem menuItem = createMenuItem((Action) action, templateContext); + menuItems.add(menuItem); + } else { + Log.e(LogTags.TEMPLATE, "Action is not supported: " + action); + } + } + return menuItems; + } + + /** Converts an {@link Action} to a {@link MenuItem}. */ + private static MenuItem createMenuItem(Action action, TemplateContext templateContext) { + CarIcon carIcon = action.getIcon(); + boolean isTinted = carIcon == null || carIcon.getType() != CarIcon.TYPE_APP_ICON; + MenuItem.Builder menuItemBuilder = + new MenuItem.Builder(templateContext) + .setPrimary(true) + .setEnabled(true) + .setTinted(isTinted) + .setShowIconAndTitle(true) + .setTitle(CarTextUtils.toCharSequenceOrEmpty(templateContext, action.getTitle())); + OnClickDelegate onClickDelegate = action.getOnClickDelegate(); + if (onClickDelegate != null) { + menuItemBuilder.setOnClickListener( + item -> CommonUtils.dispatchClick(templateContext, onClickDelegate)); + } + + MenuItem menuItem = menuItemBuilder.build(); + + int menuItemIconSize = + (int) + templateContext + .getResources() + .getDimension(com.android.car.ui.R.dimen.car_ui_toolbar_menu_item_icon_size); + carIcon = ImageUtils.getIconFromAction(action); + if (carIcon != null) { + ImageViewParams imageViewParams; + CarColor tintColor = carIcon.getTint(); + if (tintColor != null && tintColor.getColor() != 0) { + imageViewParams = + ImageViewParams.builder() + .setDefaultTint(tintColor.getColor()) + .setForceTinting(true) + .build(); + } else { + imageViewParams = ImageViewParams.DEFAULT; + } + + ImageUtils.setImageTargetSrc( + templateContext, + carIcon, + menuItem::setIcon, + imageViewParams, + menuItemIconSize, + menuItemIconSize); + } + return menuItem; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonListView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonListView.java new file mode 100644 index 0000000..96b4c15 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonListView.java @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import static com.android.car.libraries.apphost.common.CarHostConfig.PRIMARY_ACTION_HORIZONTAL_ORDER_LEFT; +import static com.android.car.libraries.apphost.common.CarHostConfig.PRIMARY_ACTION_HORIZONTAL_ORDER_NOT_SET; +import static com.android.car.libraries.apphost.common.CarHostConfig.PRIMARY_ACTION_HORIZONTAL_ORDER_RIGHT; +import static java.lang.Math.min; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; +import androidx.annotation.StyleableRes; +import androidx.annotation.VisibleForTesting; +import androidx.car.app.model.Action; +import androidx.car.app.model.CarColor; +import androidx.car.app.model.CarIcon; +import androidx.car.app.model.CarText; +import com.android.car.libraries.apphost.common.CarColorUtils; +import com.android.car.libraries.apphost.common.CarHostConfig.PrimaryActionOrdering; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.view.common.ActionButtonListParams; +import com.android.car.libraries.apphost.view.common.CarTextUtils; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonView.ActionFlag; +import java.util.ArrayList; +import java.util.List; + +/** Displays a list of {@link Action}s as buttons in a single horizontal layout. */ +public class ActionButtonListView extends LinearLayout { + + public enum Gravity { + /* Indicates that action button list can be rendered within the content. */ + CENTER, + + /* Indicates that action button list should be pinned to the bottom of the content. */ + BOTTOM + } + + /** A flag that indicates whether the buttons stretch horizontally to fill the available space. */ + private final boolean mButtonsStretch; + + /** + * The maximum button width. + * + * <p>This limit is only applied when {@link #mButtonsStretch} is set to {@code true}. + */ + private final int mButtonMaxWidth; + + /** The horizontal spacing between the button list and the parent view. */ + private final int mHorizontalSpacing; + + /** Minimum touch area for each button in this list */ + private final int mMinTouchTargetSize; + + @ColorInt private final int mDefaultButtonBackgroundColor; + + public ActionButtonListView(Context context) { + this(context, null); + } + + public ActionButtonListView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public ActionButtonListView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ActionButtonListView( + Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + @StyleableRes + final int[] themeAttrs = { + R.attr.templateActionButtonListButtonStretchHorizontal, + R.attr.templateActionButtonListButtonMaxWidth, + R.attr.templatePlainContentHorizontalPadding, + R.attr.templateActionButtonTouchTargetSize, + R.attr.templateActionButtonDefaultBackgroundColor, + }; + + TypedArray ta = context.obtainStyledAttributes(themeAttrs); + mButtonsStretch = ta.getBoolean(0, false); + mButtonMaxWidth = ta.getDimensionPixelSize(1, 0); + mHorizontalSpacing = ta.getDimensionPixelSize(2, 0); + mMinTouchTargetSize = ta.getDimensionPixelSize(3, 0); + mDefaultButtonBackgroundColor = ta.getColor(4, 0); + ta.recycle(); + } + + /** Returns the {@link ActionButtonView} for testing. */ + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public ActionButtonView getActionButtonView(int index) { + int maxIndex = getChildCount() - 1; + if (index > maxIndex || index < 0) { + throw new IndexOutOfBoundsException( + "Action index is not within bounds of [0, " + maxIndex + "]"); + } + + View child = getChildAt(index); + if (child instanceof ActionButtonView) { + return (ActionButtonView) child; + } + throw new IllegalStateException( + "Found unexpected type of view in action list: " + child.getClass()); + } + + /** Returns the size of the list for testing. */ + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public int size() { + return getChildCount(); + } + + /** + * Sets the {@link Action}s that will be mapped into buttons. + * + * @see ActionFlag + */ + public void setActionList( + TemplateContext templateContext, List<Action> actionList, ActionButtonListParams params) { + LayoutInflater inflater = LayoutInflater.from(getContext()); + + removeAllViews(); + + if (actionList == null || actionList.isEmpty()) { + this.setVisibility(View.GONE); + return; + } + + setVisibility(View.VISIBLE); + + @StyleableRes final int[] themeAttrs = {R.attr.templateActionButtonMargin}; + TypedArray ta = templateContext.obtainStyledAttributes(themeAttrs); + int actionMargin = ta.getDimensionPixelOffset(0, 0); + ta.recycle(); + + int maxActions = params.getMaxActions(); + if (actionList.size() > maxActions) { + L.w( + LogTags.TEMPLATE, + "The number of actions exceeds the maximum allowed action count, skipping later actions"); + actionList = actionList.subList(0, maxActions); + } + + if (params.allowOemReordering()) { + int primaryActionOrder = templateContext.getCarHostConfig().getPrimaryActionOrder(); + actionList = reorderActionList(actionList, primaryActionOrder); + } + + // Calculate the stretching button width by subtracting the margins and paddings from the + // screen width, and dividing the remaining width by the button count. Then cap the + // resulting width at the button max width. + int screenWidth = getResources().getDisplayMetrics().widthPixels; + int buttonCount = actionList.size(); + int stretchingButtonWidth = + min( + (screenWidth - actionMargin * (buttonCount - 1) - 2 * mHorizontalSpacing) / buttonCount, + mButtonMaxWidth); + + ViewGroup touchContainer = (ViewGroup) getParent(); + touchContainer.setTouchDelegate(null); + + boolean allowAppColor = + getAllowAppColor(templateContext, actionList, params, mDefaultButtonBackgroundColor); + params = ActionButtonListParams.builder(params).setAllowAppColor(allowAppColor).build(); + + int count = 0; + for (Action action : actionList) { + View view = inflater.inflate(R.layout.action_button_view, this, false); + ((ActionButtonView) view).setAction(templateContext, action, params); + + LayoutParams layoutParams = (LayoutParams) view.getLayoutParams(); + if (count > 0) { + layoutParams.setMarginStart(actionMargin); + } + + if (mButtonsStretch) { + layoutParams.width = stretchingButtonWidth; + } + + addView(view, layoutParams); + ViewUtils.setMinTapTarget(touchContainer, view, mMinTouchTargetSize); + ++count; + } + } + + /** Returns whether app-provided colors can be applied to the buttons. */ + private static boolean getAllowAppColor( + TemplateContext templateContext, + List<Action> actionList, + ActionButtonListParams params, + @ColorInt int defaultButtonBackgroundColor) { + if (templateContext.getCarHostConfig().isButtonColorOverriddenByOEM() + && params.allowOemColorOverride()) { + // OEM overrides app colors + return false; + } + + // Allow app colors only if the contrast check passes + return checkColorContrast( + templateContext, actionList, params.getSurroundingColor(), defaultButtonBackgroundColor); + } + + /** + * Checks the color contrast between contents of the given action list and the background color. + */ + private static boolean checkColorContrast( + TemplateContext templateContext, + List<Action> actionList, + @ColorInt int surroundingColor, + @ColorInt int defaultButtonBackgroundColor) { + for (Action action : actionList) { + // Check if the background color has enough contrast against the surrounding color. + CarColor backgroundCarColor = action.getBackgroundColor(); + if (backgroundCarColor != null) { + if (!CarColorUtils.checkColorContrast( + templateContext, backgroundCarColor, surroundingColor)) { + return false; + } + } + + // Check if the text color has enough contrast against the background color. + @ColorInt + int backgroundColor = + ActionButtonViewUtils.getBackgroundColor( + templateContext, + action, + /* surroundingColor= */ surroundingColor, + /* defaultBackgroundColor= */ defaultButtonBackgroundColor); + CarText title = action.getTitle(); + if (title != null) { + if (!CarTextUtils.checkColorContrast(templateContext, title, backgroundColor)) { + return false; + } + } + + // Check if the icon tint has enough contrast against the background color. + CarIcon icon = action.getIcon(); + if (icon != null) { + CarColor tint = icon.getTint(); + if (tint != null) { + if (!CarColorUtils.checkColorContrast(templateContext, tint, backgroundColor)) { + return false; + } + } + } + } + return true; + } + + /** {@link ActionButtonView}s will carry out the action when clicked on without toast. */ + public void enableActionButtons() { + for (int index = 0; index < getChildCount(); index++) { + View child = getChildAt(index); + if (child instanceof ActionButtonView) { + ActionButtonView button = (ActionButtonView) child; + button.enableActionButton(); + } + } + } + + /** + * {@link ActionButtonView}s will show a toast with given message instead of carrying out the + * action when clicked on. + */ + public void disableActionButtons(String disabledToastMessage) { + for (int index = 0; index < getChildCount(); index++) { + View child = getChildAt(index); + if (child instanceof ActionButtonView) { + ActionButtonView button = (ActionButtonView) child; + button.disableActionButton(disabledToastMessage); + } + } + } + + private List<Action> reorderActionList( + List<Action> actionList, @PrimaryActionOrdering int primaryActionOrder) { + ArrayList<Action> mutableActionList = new ArrayList<>(actionList); + if (primaryActionOrder == PRIMARY_ACTION_HORIZONTAL_ORDER_NOT_SET) { + return actionList; + } + int indexOfPrimaryAction = 0; + @Nullable Action primaryAction = null; + for (Action action : mutableActionList) { + if (ActionButtonViewUtils.isPrimaryAction(action)) { + primaryAction = action; + break; + } + indexOfPrimaryAction++; + } + if (primaryAction != null) { + mutableActionList.remove(indexOfPrimaryAction); + if (primaryActionOrder == PRIMARY_ACTION_HORIZONTAL_ORDER_LEFT) { + mutableActionList.add(0, primaryAction); + } else if (primaryActionOrder == PRIMARY_ACTION_HORIZONTAL_ORDER_RIGHT) { + mutableActionList.add(primaryAction); + } + } + return mutableActionList; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonView.java new file mode 100644 index 0000000..fdeb929 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonView.java @@ -0,0 +1,381 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import static androidx.car.app.model.Action.TYPE_BACK; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.Toast; +import androidx.annotation.ColorInt; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import androidx.annotation.StyleableRes; +import androidx.annotation.VisibleForTesting; +import androidx.car.app.model.Action; +import androidx.car.app.model.CarColor; +import androidx.car.app.model.CarIcon; +import androidx.car.app.model.CarText; +import androidx.car.app.model.OnClickDelegate; +import com.android.car.libraries.apphost.common.CommonUtils; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.distraction.constraints.CarColorConstraints; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction; +import com.android.car.libraries.apphost.view.common.ActionButtonListParams; +import com.android.car.libraries.apphost.view.common.CarTextParams; +import com.android.car.libraries.apphost.view.common.CarTextUtils; +import com.android.car.libraries.apphost.view.common.ImageUtils; +import com.android.car.libraries.apphost.view.common.ImageViewParams; +import com.android.car.libraries.templates.host.R; +import com.android.car.ui.widget.CarUiTextView; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Displays an {@link Action} as a button. */ +public class ActionButtonView extends FrameLayout { + private static final int[] BUTTON_PRIMARY = + new int[] {R.attr.type_primary}; + private static final int[] BUTTON_CUSTOM = + new int[] {R.attr.type_custom}; + private static final int[] BUTTON_CUSTOM_PRIMARY = + new int[] { + R.attr.type_custom, + R.attr.type_primary + }; + + @IntDef( + flag = true, + value = { + FLAG_SUPPORT_REORDERING_BY_OEM, + FLAG_SUPPORT_CUSTOMIZED_COLOR_BY_OEM, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ActionFlag {} + + public static final int FLAG_SUPPORT_REORDERING_BY_OEM = 1 << 0; + public static final int FLAG_SUPPORT_CUSTOMIZED_COLOR_BY_OEM = 1 << 1; + + @ColorInt private final int mDefaultBackgroundColor; + @ColorInt private final int mDefaultIconTint; + private final int mMinWidthWithText; + private final int mMinWidthWithoutText; + private final int mSideAlignmentSpacing; + private final int mCustomMaxEms; + private boolean mIsPrimary; + private boolean mIsCustom; + private boolean mIsEnabled; + private String mDisabledToastMessage; + + /** + * The content alignment. + * + * <p>The possible values are: + * + * <ul> + * <li>0: center (default) + * <li>1: left + * <li>2: right + * </ul> + */ + private final int mContentAlignment; + + /** A flag that indicates whether the buttons stretch horizontally to fill the available space. */ + private final boolean mButtonsStretch; + + public ActionButtonView(Context context) { + this(context, null); + } + + public ActionButtonView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public ActionButtonView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ActionButtonView( + Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + @StyleableRes + final int[] themeAttrs = { + R.attr.templateActionButtonDefaultBackgroundColor, + R.attr.templateActionDefaultIconTint, + R.attr.templateActionWithTextMinWidth, + R.attr.templateActionWithoutTextMinWidth, + R.attr.templateActionButtonSideAlignmentSpacing, + R.attr.templateActionButtonListButtonContentAlignment, + R.attr.templateActionButtonListButtonStretchHorizontal, + }; + + TypedArray ta = context.obtainStyledAttributes(themeAttrs); + mDefaultBackgroundColor = ta.getColor(0, 0); + mDefaultIconTint = ta.getColor(1, 0); + mMinWidthWithText = ta.getDimensionPixelSize(2, 0); + mMinWidthWithoutText = ta.getDimensionPixelSize(3, 0); + mSideAlignmentSpacing = ta.getDimensionPixelSize(4, 0); + mContentAlignment = ta.getInteger(5, 0); + mButtonsStretch = ta.getBoolean(6, false); + ta.recycle(); + + // TODO(b/184195457): remove the custom maxEms limit when we remove the limit for all + ta = + context.obtainStyledAttributes( + attrs, R.styleable.ActionButtonView, defStyleAttr, defStyleRes); + mCustomMaxEms = ta.getInt(R.styleable.ActionButtonView_textMaxEms, 0); + ta.recycle(); + + mIsEnabled = true; + } + + /** Returns the {@link android.view.View} title for testing. */ + @Nullable + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public String getTitle() { + CarUiTextView carUiTextView = findViewById(R.id.action_text); + if (carUiTextView == null) { + return null; + } + return carUiTextView.getText().toString(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + } + + /** Updates the view from the {@link Action} model. */ + public ActionButtonView setAction( + TemplateContext templateContext, Action action, ActionButtonListParams params) { + L.v(LogTags.TEMPLATE, "Setting action view with action: %s", action); + + removeAllViews(); + + final boolean allowAppColor = params.allowAppColor(); + + // Set the background color + final CarColor color = action.getBackgroundColor(); + @ColorInt int appBackgroundColor = mDefaultBackgroundColor; + if (color != null && allowAppColor) { + appBackgroundColor = + ActionButtonViewUtils.getBackgroundColor( + templateContext, + action, + /* surroundingColor= */ params.getSurroundingColor(), + /* defaultBackgroundColor= */ mDefaultBackgroundColor); + } + + final boolean useAppColors = appBackgroundColor != mDefaultBackgroundColor; + if (useAppColors) { + // Set the background as tint to not override the round-corner drawable with ripple effects. + setBackgroundTintList(ColorStateList.valueOf(appBackgroundColor)); + } + + boolean useOemColor = + templateContext.getCarHostConfig().isButtonColorOverriddenByOEM() + && params.allowOemColorOverride(); + updateState( + /* isCustom= */ useAppColors, + /* isPrimary= */ useOemColor && ActionButtonViewUtils.isPrimaryAction(action)); + + // Check if the title's color span has enough contrast against the background color + final CarColorConstraints textColorConstraints; + CarText titleText = action.getTitle(); + if (allowAppColor + && titleText != null + && CarTextUtils.checkColorContrast(templateContext, titleText, appBackgroundColor)) { + textColorConstraints = CarColorConstraints.UNCONSTRAINED; + } else { + textColorConstraints = CarColorConstraints.NO_COLOR; + } + + final CarTextParams carTextParams = + CarTextParams.builder() + .setColorSpanConstraints(textColorConstraints) + .setBackgroundColor(appBackgroundColor) + .build(); + CharSequence title = + CarTextUtils.toCharSequenceOrEmpty(templateContext, titleText, carTextParams); + CarIcon icon = ImageUtils.getIconFromAction(action); + + boolean hasTitle = title.length() > 0; + + setMinimumWidth(hasTitle ? mMinWidthWithText : mMinWidthWithoutText); + + LayoutInflater inflater = LayoutInflater.from(getContext()); + if (icon != null && hasTitle) { + inflater.inflate(R.layout.action_button_view_icon_text, this); + } else if (hasTitle) { + inflater.inflate(R.layout.action_button_view_text, this); + } else { + inflater.inflate(R.layout.action_button_view_icon, this); + } + + if (icon != null) { + ImageView iconView = findViewById(R.id.action_icon); + ImageViewParams imageViewParams = + ImageViewParams.builder() + .setDefaultTint(mDefaultIconTint) + .setForceTinting(true) + .setIgnoreAppTint(!allowAppColor) + .setBackgroundColor(appBackgroundColor) + .build(); + ImageUtils.setImageSrc(templateContext, icon, iconView, imageViewParams); + } + + if (hasTitle) { + CarUiTextView carUiTextView = findViewById(R.id.action_text); + if (mCustomMaxEms > 0) { + carUiTextView.setMaxEms(mCustomMaxEms); + } else if (mButtonsStretch) { + // If max EMS is not set and the button stretches, allow the buttons to fill all + // available space. + carUiTextView.setMaxWidth(Integer.MAX_VALUE); + } + + carUiTextView.setText( + CarUiTextUtils.fromCarText( + templateContext, action.getTitle(), carTextParams, carUiTextView.getMaxLines())); + } + + // Update the click listener, if one is set. + if (action.getType() == TYPE_BACK) { + setOnClickListener( + v -> { + if (!mIsEnabled) { + showDisabledToast(templateContext); + return; + } + + templateContext.getBackPressedHandler().onBackPressed(); + }); + } else { + OnClickDelegate onClickDelegate = action.getOnClickDelegate(); + if (onClickDelegate != null) { + setOnClickListener( + v -> { + ViewUtils.logCarAppTelemetry(templateContext, UiAction.ACTION_BUTTON_CLICKED); + + if (!mIsEnabled) { + showDisabledToast(templateContext); + return; + } + + CommonUtils.dispatchClick(templateContext, onClickDelegate); + }); + } else { + setOnClickListener(null); + } + } + + // Set the content alignment and margins + View contentView = getChildAt(0); + if (contentView != null) { + FrameLayout.LayoutParams layoutParams = + (FrameLayout.LayoutParams) contentView.getLayoutParams(); + int contentGravity = getContentGravity(mContentAlignment); + layoutParams.gravity = contentGravity; + + // If the content is aligned to the side, use side-alignment-specific horizontal + // margins. + if (contentGravity != Gravity.CENTER) { + layoutParams.leftMargin = mSideAlignmentSpacing; + layoutParams.rightMargin = mSideAlignmentSpacing; + } + + contentView.setLayoutParams(layoutParams); + } + + return this; + } + + /** {@link ActionButtonView} will carry out the action when clicked on without toast. */ + public void enableActionButton() { + mIsEnabled = true; + } + + /** + * {@link ActionButtonView} will show a toast with given message instead of carrying out the + * action when clicked on. + */ + public void disableActionButton(String disabledToastMessage) { + mIsEnabled = false; + mDisabledToastMessage = disabledToastMessage; + } + + private void showDisabledToast(TemplateContext templateContext) { + templateContext.getToastController().showToast(mDisabledToastMessage, Toast.LENGTH_SHORT); + } + + /** Returns {@code true} if action button is enabled. */ + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public boolean isActionButtonEnabled() { + return mIsEnabled; + } + + /** Gets the gravity value that corresponds to the content alignment value. */ + private static int getContentGravity(int contentAlignment) { + int gravity = Gravity.CENTER_VERTICAL; + switch (contentAlignment) { + case 1: + gravity |= Gravity.LEFT; + break; + case 2: + gravity |= Gravity.RIGHT; + break; + case 0: // fall-through + default: + gravity = Gravity.CENTER; + } + + return gravity; + } + + @Override + protected int[] onCreateDrawableState(int extraSpace) { + int[] additionalStates; + if (mIsPrimary && mIsCustom) { + additionalStates = BUTTON_CUSTOM_PRIMARY; + } else if (mIsPrimary) { + additionalStates = BUTTON_PRIMARY; + } else if (mIsCustom) { + additionalStates = BUTTON_CUSTOM; + } else { + return super.onCreateDrawableState(extraSpace); + } + int[] state = super.onCreateDrawableState(extraSpace + additionalStates.length); + mergeDrawableStates(state, additionalStates); + return state; + } + + private void updateState(boolean isCustom, boolean isPrimary) { + if (isCustom != mIsCustom || isPrimary != mIsPrimary) { + mIsCustom = isCustom; + mIsPrimary = isPrimary; + refreshDrawableState(); + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonViewUtils.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonViewUtils.java new file mode 100644 index 0000000..596f9d7 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonViewUtils.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import androidx.annotation.ColorInt; +import androidx.car.app.model.Action; +import com.android.car.libraries.apphost.common.CarColorUtils; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.distraction.constraints.CarColorConstraints; + +/** Util class for {@link ActionButtonView}. */ +final class ActionButtonViewUtils { + + /** Returns whether the given action is a primary action. */ + static boolean isPrimaryAction(Action action) { + return (action.getFlags() & Action.FLAG_PRIMARY) != 0; + } + + /** Returns the background color of the given action. */ + static int getBackgroundColor( + TemplateContext templateContext, + Action action, + @ColorInt int surroundingColor, + @ColorInt int defaultBackgroundColor) { + return CarColorUtils.resolveColor( + templateContext, + /* carColor= */ action.getBackgroundColor(), + /* isDark= */ true, + /* defaultColor= */ defaultBackgroundColor, + /* constraints= */ CarColorConstraints.UNCONSTRAINED, + /* backgroundColor= */ surroundingColor); + } + + private ActionButtonViewUtils() {} +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionListUtils.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionListUtils.java new file mode 100644 index 0000000..4776734 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionListUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import androidx.car.app.model.Action; +import java.util.ArrayList; +import java.util.List; + +/** Util class for {@link Action} lists. */ +public final class ActionListUtils { + /** + * Returns whether the given object is a list of {@link Action}s or not. + * + * <p>An empty list is not considered an action list. + */ + @SuppressWarnings("unchecked") + public static boolean isActionList(Object obj) { + if (!(obj instanceof List)) { + return false; + } + + List<Object> list = (List) obj; + if (list.isEmpty()) { + return false; + } + + // Only check if the first element is an action. When we create a list of actions later, we + // will + // skip non-action elements. + return list.get(0) instanceof Action; + } + + /** + * Returns a list of {@link Action}s if the given object is an action list, and an empty list if + * it is not. + */ + @SuppressWarnings("unchecked") + public static List<Action> getActionList(Object obj) { + List<Action> actionList = new ArrayList<>(); + if (obj instanceof List) { + List<Object> list = (List) obj; + for (Object element : list) { + if (element instanceof Action) { + actionList.add((Action) element); + } + } + } + + return actionList; + } + + private ActionListUtils() {} +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionStripUtils.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionStripUtils.java new file mode 100644 index 0000000..e4beced --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionStripUtils.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import androidx.annotation.Nullable; +import androidx.car.app.model.Action; +import androidx.car.app.model.ActionStrip; +import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints; +import com.android.car.libraries.apphost.template.view.model.ActionStripWrapper; +import com.android.car.libraries.apphost.template.view.model.ActionWrapper; +import com.google.common.collect.ImmutableList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** Util class for {@link ActionStrip}. */ +final class ActionStripUtils { + /** + * Validates the {@link ActionStrip} against the {@link ActionsConstraints} instance's required + * types. + * + * @throws ValidationException if the action strip does not meet the required type constraints. + */ + static void validateRequiredTypes( + @Nullable ActionStripWrapper actionStrip, ActionsConstraints constraints) + throws ValidationException { + List<ActionWrapper> actions = + actionStrip == null ? ImmutableList.of() : actionStrip.getActions(); + + // Check for any missing required types. + Set<Integer> requiredActionTypes = constraints.getRequiredActionTypes(); + if (!requiredActionTypes.isEmpty()) { + Set<Integer> requiredTypes = new HashSet<>(requiredActionTypes); + + for (ActionWrapper action : actions) { + requiredTypes.remove(action.get().getType()); + } + + if (!requiredTypes.isEmpty()) { + StringBuilder missingTypeError = new StringBuilder(); + for (int type : requiredTypes) { + missingTypeError.append(Action.typeToString(type)).append(";"); + } + throw new ValidationException("Missing required action types: " + missingTypeError); + } + } + } + + private ActionStripUtils() {} + + /** An exception thrown if the action strip validation fails. */ + static class ValidationException extends Exception { + private ValidationException(String errorMessage) { + super(errorMessage); + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionStripView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionStripView.java new file mode 100644 index 0000000..e1f9ed1 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionStripView.java @@ -0,0 +1,461 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import static android.widget.LinearLayout.VERTICAL; +import static com.android.car.libraries.apphost.template.view.model.ActionStripWrapper.INVALID_FOCUSED_ACTION_INDEX; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.util.concurrent.TimeUnit.SECONDS; + +import android.animation.Animator; +import android.animation.AnimatorInflater; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.content.ComponentName; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; +import androidx.annotation.StyleableRes; +import androidx.car.app.model.Action; +import androidx.car.app.model.ActionStrip; +import androidx.car.app.model.CarText; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; +import com.android.car.libraries.apphost.common.CarAppError; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.common.ThreadUtils; +import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction; +import com.android.car.libraries.apphost.template.view.model.ActionStripWrapper; +import com.android.car.libraries.apphost.template.view.model.ActionWrapper; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.view.widgets.common.ActionStripUtils.ValidationException; +import java.util.ArrayList; +import java.util.List; + +/** A view that displays an action strip for the templates. */ +public class ActionStripView extends FrameLayout { + public static final long ACTIONSTRIP_ACTIVE_STATE_DURATION_MILLIS = SECONDS.toMillis(10); + private static final int MSG_ACTIONSTRIP_ACTIVE_STATE = 1; + + /** A delegate that responds to the visibility updates due to active state changes. */ + public interface ActiveStateDelegate { + /** Invoked when the view's visibility changes due to the active state change. */ + void onActiveStateVisibilityChanged(); + } + + private final Handler mHandler = new Handler(new HandlerCallback()); + + private boolean mIsActive = true; + private boolean mAllowTwoLines = false; + private LinearLayout mPrimaryContainer; + private LinearLayout mSecondaryContainer; + private ViewGroup mTouchContainer; + private final int mButtonMargin; + private final int mButtonHeight; + private final int mMinTouchTargetSize; + private TemplateContext mTemplateContext; + @StyleRes private final int mFabStyleResId; + + @Nullable private ActiveStateDelegate mActiveStateDelegate; + + @Nullable ComponentName mAppName; + + public ActionStripView(Context context) { + this(context, null); + } + + public ActionStripView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public ActionStripView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + @SuppressWarnings({"ResourceType"}) + public ActionStripView( + Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + @StyleableRes + final int[] themeAttrs = { + R.attr.templateActionStripButtonMargin, + R.attr.templateActionButtonHeight, + R.attr.templateActionButtonTouchTargetSize, + }; + + TypedArray ta = context.obtainStyledAttributes(themeAttrs); + mButtonMargin = ta.getDimensionPixelSize(0, 0); + mButtonHeight = ta.getDimensionPixelOffset(1, 0); + mMinTouchTargetSize = ta.getDimensionPixelOffset(2, 0); + ta.recycle(); + + // Get the fab appearance style resource id from the view's attributes. + TypedArray viewStyledAttributes = + context.obtainStyledAttributes( + attrs, R.styleable.ActionStripView, defStyleAttr, defStyleRes); + mFabStyleResId = + viewStyledAttributes.getResourceId(R.styleable.ActionStripView_fabAppearance, -1); + viewStyledAttributes.recycle(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mPrimaryContainer = findViewById(R.id.action_strip_container); + mSecondaryContainer = findViewById(R.id.action_strip_container_secondary); + mTouchContainer = findViewById(R.id.action_strip_touch_container); + } + + /** Returns whether the buttons are allowed to be arranged in two lines. */ + public boolean getAllowTwoLines() { + return mAllowTwoLines; + } + + /** Sets the {@link ActiveStateDelegate} for this action strip view. */ + public void setActiveStateDelegate(ActiveStateDelegate activeStateDelegate) { + this.mActiveStateDelegate = activeStateDelegate; + } + + /** + * Updates the {@link ActionStrip} to the {@link ActionStripView}. + * + * @see {@link #setActionStrip(TemplateContext, ActionStrip, ActionsConstraints, boolean)}. + */ + public void setActionStrip( + TemplateContext templateContext, + @Nullable ActionStrip actionStrip, + ActionsConstraints constraints) { + setActionStrip(templateContext, actionStrip, constraints, false); + } + + /** + * Updates the {@link ActionStrip} to the {@link ActionStripView}. + * + * @see {@link #setActionStrip(TemplateContext, ActionStrip, ActionsConstraints, boolean)}. + */ + public void setActionStrip( + TemplateContext templateContext, + @Nullable ActionStrip actionStrip, + ActionsConstraints constraints, + boolean allowTwoLines) { + ActionStripWrapper actionStripWrapper = + actionStrip == null ? null : new ActionStripWrapper.Builder(actionStrip).build(); + setActionStrip(templateContext, actionStripWrapper, constraints, allowTwoLines); + } + + /** + * Updates the {@link ActionStrip} to the {@link ActionStripView}. + * + * <p>The {@link ActionStrip} will be validated against the given {@link ActionsConstraints} + * instance. If the number of {@link Action}s in the action strip exceeds the max allowed actions + * as specified in the constraints, the {@link Action}s beyond the allowed number will be dropped + * from the view. + * + * <p>If the {@link ActionStrip} is {@code null} or if there are no {@link Action}s added to the + * view, the action strip will be hidden. + * + * <p>If {@code allowTwoLines} is {@code true}, the buttons are positioned in two lines. The last + * two actions will be in the primary container, and the rest in the secondary container. + */ + public void setActionStrip( + TemplateContext templateContext, + @Nullable ActionStripWrapper actionStrip, + ActionsConstraints constraints, + boolean allowTwoLines) { + mAllowTwoLines = allowTwoLines; + mAppName = templateContext.getCarAppPackageInfo().getComponentName(); + mTemplateContext = templateContext; + // Ensure the model satisfies the input constraints. + try { + ActionStripUtils.validateRequiredTypes(actionStrip, constraints); + } catch (ValidationException exception) { + templateContext + .getErrorHandler() + .showError( + CarAppError.builder(templateContext.getCarAppPackageInfo().getComponentName()) + .setCause(exception) + .build()); + } + + if (actionStrip == null) { + setVisibility(GONE); + return; + } + + // Set the host-determined index of the action button to focus. Otherwise, if a button was + // focused, get its index before removing the button views. + int focusedActionIndex = + actionStrip.getFocusedActionIndex() == INVALID_FOCUSED_ACTION_INDEX + ? getCurrentFocusedActionIndex() + : actionStrip.getFocusedActionIndex(); + mPrimaryContainer.removeAllViews(); + mSecondaryContainer.removeAllViews(); + + int maxAllowedActions = constraints.getMaxActions(); + int maxAllowedCustomTitles = constraints.getMaxCustomTitles(); + List<ActionWrapper> actions = actionStrip.getActions(); + List<ActionWrapper> allowedActions = new ArrayList<>(); + + for (ActionWrapper action : actions) { + CarText title = action.get().getTitle(); + if (title != null && !title.isEmpty()) { + if (--maxAllowedCustomTitles < 0) { + L.w( + LogTags.TEMPLATE, + "Dropping actions in action strip over max custom title limit of %d", + constraints.getMaxCustomTitles()); + break; + } + } + + if (--maxAllowedActions < 0) { + L.w( + LogTags.TEMPLATE, + "Dropping actions in action strip over max limit of %d", + constraints.getMaxActions()); + break; + } + + allowedActions.add(action); + } + + // Go through the actions in reverse, and add them to the appropriate containers. If two + // lines are allowed, the last two actions will be in the primary container, and the rest in + // the secondary container. + int lastPrimaryContainerActionIndex = allowTwoLines ? max(allowedActions.size() - 2, 0) : 0; + for (int i = allowedActions.size() - 1; i >= 0; i--) { + ActionWrapper action = allowedActions.get(i); + + FabView fabView = new FabView(getContext(), null, 0, mFabStyleResId); + LinearLayout container = + i >= lastPrimaryContainerActionIndex ? mPrimaryContainer : mSecondaryContainer; + container.addView(fabView, 0); + + // Set the action on a fab view. + fabView.setAction(templateContext, action); + } + + updateFabViewLayoutParams(mPrimaryContainer); + updateFabViewLayoutParams(mSecondaryContainer); + + List<View> actionButtons = getActionButtons(); + int actionCount = actionButtons.size(); + if (actionCount < 1) { + setVisibility(GONE); + } else { + // If a button was focused before, restore the focus. + if (focusedActionIndex >= 0) { + int indexToFocus = min(focusedActionIndex, actionCount - 1); + actionButtons.get(indexToFocus).requestFocus(); + } + + mPrimaryContainer.setVisibility(mPrimaryContainer.getChildCount() > 0 ? VISIBLE : GONE); + mSecondaryContainer.setVisibility(mSecondaryContainer.getChildCount() > 0 ? VISIBLE : GONE); + + // Synchronize the visibility and the FABs clickable states with the active/idle state, + // and do not show the strip's buttons unless it is current active. + setVisibility(mIsActive ? VISIBLE : GONE); + setFabViewClickableState(mIsActive); + } + + updateTouchTarget(); + + ViewUtils.logCarAppTelemetry(templateContext, UiAction.ACTION_STRIP_SIZE, actionCount); + } + + /** + * Requests the action strip is active after the specified delay. + * + * <p>When {@code true}, the action strip fades in if it is not currently visible. If {@code + * false}, the action strip fades out. + * + * <p>If there is a currently pending request to activate/de-activate the action strip that has + * not been processed yet, the previous request will be cancelled and the new request will be + * queued. + */ + public void setActiveStateWithDelay(boolean isActive, long millis) { + mHandler.removeMessages(MSG_ACTIONSTRIP_ACTIVE_STATE); + Message message = mHandler.obtainMessage(MSG_ACTIONSTRIP_ACTIVE_STATE); + message.obj = isActive; + mHandler.sendMessageDelayed(message, millis); + } + + /** + * Sets whether the action strip is active. + * + * <p>This will immediately activate/de-activate the action strip and cancel any pending requests + * that might have been sent via {@link #setActiveStateWithDelay}. + */ + public void setActiveState(boolean isActive) { + mHandler.removeMessages(MSG_ACTIONSTRIP_ACTIVE_STATE); + setActionStateInternal(isActive); + } + + private void setActionStateInternal(boolean isActive) { + if (mIsActive == isActive) { + return; + } + + if (mTemplateContext != null) { + ViewUtils.logCarAppTelemetry( + mTemplateContext, isActive ? UiAction.ACTION_STRIP_SHOW : UiAction.ACTION_STRIP_HIDE); + } + + mIsActive = isActive; + + List<Animator> animations = new ArrayList<>(); + boolean isVisible = isActive && !getActionButtons().isEmpty(); + int animResId = + isVisible ? R.anim.fab_view_animation_fade_in : R.anim.fab_view_animation_fade_out; + for (View actionButton : getActionButtons()) { + Animator animation = AnimatorInflater.loadAnimator(getContext(), animResId); + animation.setTarget(actionButton); + animations.add(animation); + } + + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(animations); + animatorSet.setInterpolator(new FastOutSlowInInterpolator()); + animatorSet.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + // Make the Fab clickable/non-clickable as soon as the animation starts. + // Updating this only after the animation has started prevents the user + // clicking in the action strip area to activate the strip, and the FAB + // responding to the same click event. + // TODO(b/165887188): add test for this. + setFabViewClickableState(isActive); + + if (isVisible) { + setVisibility(VISIBLE); + + ActiveStateDelegate delegate = mActiveStateDelegate; + if (delegate != null) { + delegate.onActiveStateVisibilityChanged(); + } + } + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!isVisible) { + setVisibility(GONE); + + ActiveStateDelegate delegate = mActiveStateDelegate; + if (delegate != null) { + delegate.onActiveStateVisibilityChanged(); + } + } + } + }); + + ThreadUtils.runOnMain(() -> animatorSet.start()); + } + + /** + * Returns the index of the focused button. + * + * <p>If none are focused, returns {@link ActionStripWrapper#INVALID_FOCUSED_ACTION_INDEX}. + */ + private int getCurrentFocusedActionIndex() { + int focusedActionIndex = INVALID_FOCUSED_ACTION_INDEX; + List<View> actionButtons = getActionButtons(); + + for (int i = 0; i < actionButtons.size(); i++) { + View fabView = actionButtons.get(i); + if (fabView.isFocused()) { + focusedActionIndex = i; + break; + } + } + + return focusedActionIndex; + } + + /** Gets all action button views in the action strip. */ + private List<View> getActionButtons() { + ArrayList<View> actionButtons = new ArrayList<>(); + for (int i = 0; i < mSecondaryContainer.getChildCount(); i++) { + actionButtons.add(mSecondaryContainer.getChildAt(i)); + } + for (int i = 0; i < mPrimaryContainer.getChildCount(); i++) { + actionButtons.add(mPrimaryContainer.getChildAt(i)); + } + return actionButtons; + } + + private void updateFabViewLayoutParams(LinearLayout container) { + for (int i = 0; i < container.getChildCount(); i++) { + FabView fabView = (FabView) container.getChildAt(i); + + LinearLayout.LayoutParams layoutParams = + new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, mButtonHeight); + + // Set the margins on buttons after the first one. + if (i > 0) { + if (container.getOrientation() == VERTICAL) { + layoutParams.topMargin = mButtonMargin; + } else { + layoutParams.leftMargin = mButtonMargin; + } + } + + fabView.setLayoutParams(layoutParams); + } + } + + private void setFabViewClickableState(boolean clickable) { + for (View actionButton : getActionButtons()) { + FabView view = (FabView) actionButton; + view.setClickable(clickable); + } + } + + private void updateTouchTarget() { + mTouchContainer.setTouchDelegate(null); + for (View actionButton : getActionButtons()) { + FabView view = (FabView) actionButton; + ViewUtils.setMinTapTarget(mTouchContainer, view, mMinTouchTargetSize); + } + } + + /** A {@link Handler.Callback} for delay activate/de-activate the action strip. */ + private class HandlerCallback implements Handler.Callback { + @Override + public boolean handleMessage(Message msg) { + if (msg.what == MSG_ACTIONSTRIP_ACTIVE_STATE) { + boolean isActive = (boolean) msg.obj; + setActionStateInternal(isActive); + } else { + L.w(LogTags.TEMPLATE, "Unknown message: %s", msg); + } + return false; + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/AndroidManifest.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/AndroidManifest.xml new file mode 100644 index 0000000..595afa7 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/AndroidManifest.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<manifest package="com.android.car.libraries.templates.host.view" + xmlns:android="http://schemas.android.com/apk/res/android"> + + <uses-sdk android:minSdkVersion="21"/> +</manifest> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/BleedingCardView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/BleedingCardView.java new file mode 100644 index 0000000..79f7750 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/BleedingCardView.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import static java.lang.Math.max; +import static java.lang.Math.min; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Outline; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.InsetDrawable; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewOutlineProvider; +import android.widget.FrameLayout; +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; +import com.android.car.libraries.apphost.common.CarColorUtils; +import com.android.car.libraries.templates.host.R; +import java.util.Arrays; + +/** + * A card view that "bleeds" through the bottom of its parent. + * + * <p>"Bleeding" means its rounded corners become square at the bottom when the card's bottom is at, + * or past its parent's bottom, thus creating an effect as if the card is "bleeding through" (or + * "peeking out of") the bottom of the parent. + */ +public class BleedingCardView extends FrameLayout { + // Percentage of the length of the card radius that the background radius is reduced by to avoid + // it showing up from underneath the foreground border and creating a subtle but ugly aliasing + // effect + private static final float BACKGROUND_RADIUS_PERCENTAGE = 0.25f; + + private final int mRadius; + private final int mBorderWidth; + @ColorInt private final int mBorderColor; + @ColorInt private int mBackgroundColor; + private final float mWidthFraction; + private final int mMinWidth; + private final int mMaxWidth; + private final int mOemWidth; + private final int mOemMaxWidth; + + public BleedingCardView(Context context) { + this(context, null); + } + + public BleedingCardView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public BleedingCardView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + @SuppressWarnings({"ResourceType", "nullness:method.invocation", "nullness:argument"}) + public BleedingCardView( + Context context, @Nullable AttributeSet attrs, int defStyleAttrs, int defStyleRes) { + super(context, attrs, defStyleAttrs, defStyleRes); + + TypedArray ta = + context.obtainStyledAttributes(attrs, R.styleable.BleedingCardView, defStyleAttrs, 0); + mBorderWidth = ta.getDimensionPixelSize(R.styleable.BleedingCardView_cardBorderWidth, 0); + mBorderColor = ta.getColor(R.styleable.BleedingCardView_cardBorderColor, 0); + @ColorInt + int backgroundColor = ta.getColor(R.styleable.BleedingCardView_cardBackgroundColor, 0); + @ColorInt int textColor = ta.getColor(R.styleable.BleedingCardView_cardTextColor, 0); + @ColorInt + int fallbackDarkBackgroundColor = + ta.getColor(R.styleable.BleedingCardView_cardFallbackDarkBackgroundColor, 0); + @ColorInt + int fallbackLightBackgroundColor = + ta.getColor(R.styleable.BleedingCardView_cardFallbackLightBackgroundColor, 0); + mRadius = ta.getDimensionPixelSize(R.styleable.BleedingCardView_cardRadius, 0); + mWidthFraction = ta.getFloat(R.styleable.BleedingCardView_cardWidthFraction, 0.f); + mMinWidth = ta.getDimensionPixelSize(R.styleable.BleedingCardView_cardMinWidth, 0); + mMaxWidth = ta.getDimensionPixelSize(R.styleable.BleedingCardView_cardMaxWidth, 0); + mOemWidth = ta.getDimensionPixelSize(R.styleable.BleedingCardView_cardOemWidth, 0); + mOemMaxWidth = ta.getDimensionPixelSize(R.styleable.BleedingCardView_cardOemMaxWidth, 0); + ta.recycle(); + + setClipToOutline(true); + + mBackgroundColor = + calculateBackgroundColor( + backgroundColor, textColor, fallbackDarkBackgroundColor, fallbackLightBackgroundColor); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + updateCardBackground(); + } + + public int getCardRadius() { + return mRadius; + } + + @ColorInt + public int getCardBackgroundColor() { + return mBackgroundColor; + } + + /** Sets the background color and triggers an update if it has changed. */ + public void setCardBackgroundColor(@ColorInt int backgroundColor) { + if (mBackgroundColor == backgroundColor) { + return; + } + mBackgroundColor = backgroundColor; + updateCardBackground(); + } + + /** Sets the card width either based on the set {@link #mOemWidth}, or {@link #mWidthFraction}. */ + private void setCardWidthIfNeeded() { + // TODO(b/162419749): Set the percent width in the xml file, without using ConstraintLayout. + if (mOemWidth > 0) { + // If the OEM defined the card width, use it after checking for min and max values. + int cardWidth = mOemWidth; + cardWidth = min(cardWidth, mOemMaxWidth); + cardWidth = max(cardWidth, mMinWidth); + getLayoutParams().width = cardWidth; + } else if (mWidthFraction > 0) { + // If the width fraction is set, use it after checking for min and max values. + int screenWidth = getResources().getDisplayMetrics().widthPixels; + int cardWidth = (int) (screenWidth * mWidthFraction); + cardWidth = min(cardWidth, mMaxWidth); + cardWidth = max(cardWidth, mMinWidth); + getLayoutParams().width = cardWidth; + } + } + + @Override + public void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + updateCardBackground(); + } + + /** Returns a background color with proper contrast ratio for the given text color. */ + @ColorInt + private int calculateBackgroundColor( + @ColorInt int backgroundColor, + @ColorInt int textColor, + @ColorInt int fallbackDarkBackgroundColor, + @ColorInt int fallbackLightBackgroundColor) { + if (CarColorUtils.hasMinimumColorContrast(textColor, backgroundColor)) { + return backgroundColor; + } else if (CarColorUtils.hasMinimumColorContrast(textColor, fallbackDarkBackgroundColor)) { + return fallbackDarkBackgroundColor; + } else { + return fallbackLightBackgroundColor; + } + } + + private Drawable createBackground(float[] radii) { + // Create a drawable for the background. + GradientDrawable backDrawable = new GradientDrawable(); + + // Reduce the radius a bit to avoid the background popping from outside of the border. + float reduction = mRadius * BACKGROUND_RADIUS_PERCENTAGE; + float[] backRadii = Arrays.copyOf(radii, 8); + for (int i = 0; i < radii.length; ++i) { + radii[i] -= reduction; + } + backDrawable.setCornerRadii(backRadii); + backDrawable.setColor(mBackgroundColor); + + return backDrawable; + } + + private Drawable createForeground(float[] radii) { + // Create the border drawable. + GradientDrawable borderDrawable = new GradientDrawable(); + + // Blend the border with the background color. This method returns a fully opaque color. We + // do + // this instead of drawing the border over the background with an alpha so that any contents + // of the card get are drawn underneath the border (e.g. the lighter rectangle we display + // over + // the lanes image) don't get blended with the border, and the border is rather of a single + // color. + borderDrawable.setStroke( + mBorderWidth, CarColorUtils.blendColorsSrc(mBorderColor, mBackgroundColor)); + borderDrawable.setCornerRadii(radii); + return borderDrawable; + } + + private void updateCardBackground() { + setCardWidthIfNeeded(); + + // Determine whether the card is bleeding, i.e. if it goes past the bottom of the parent. + boolean isBleeding = isBleeding(); + + // Remove the bottom rounded corners if the card is bleeding. + float bottomRadius = isBleeding ? 0 : mRadius; + float[] radii = + new float[] { + mRadius, mRadius, mRadius, mRadius, bottomRadius, bottomRadius, bottomRadius, bottomRadius + }; + + // Set the background. + setBackground(createBackground(radii)); + + // Set the foreground border. + Drawable foreground = createForeground(radii); + if (isBleeding) { + // If bleeding, inset the bottom with a negative value to hide the bottom border. + foreground = new InsetDrawable(foreground, 0, 0, 0, -mBorderWidth); + } + setForeground(foreground); + + // Set the card view's outline with rounded corners to clip its child views (e.g. junction + // image). + ViewOutlineProvider outlineProvider = + new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + int bottom = view.getHeight(); + if (isBleeding()) { + // If the card view is bleeding, add the radius value so that only the + // top corners are rounded. + bottom += mRadius; + } + outline.setRoundRect(0, 0, view.getWidth(), bottom, mRadius); + } + }; + setOutlineProvider(outlineProvider); + + invalidate(); + } + + private boolean isBleeding() { + ViewGroup parent = (ViewGroup) getParent(); + boolean isBleeding = false; + if (parent != null) { + int parentHeight = parent.getHeight(); + int bottom = getTop() + getHeight(); + if (bottom >= parentHeight) { + isBleeding = true; + } + } + return isBleeding; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarEditText.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarEditText.java new file mode 100644 index 0000000..ea8764e --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarEditText.java @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Build; +import android.os.Build.VERSION_CODES; +import android.text.InputType; +import android.util.AttributeSet; +import android.view.ActionMode; +import android.view.ActionMode.Callback; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputConnectionWrapper; +import android.widget.EditText; +import android.widget.TextView; +import androidx.annotation.Nullable; +import com.android.car.libraries.apphost.input.CarEditable; +import com.android.car.libraries.apphost.input.CarEditableListener; +import com.android.car.libraries.apphost.input.InputManager; +import com.android.car.libraries.templates.host.R; + +/** + * A EditText for use in-car. This EditText: + * + * <ul> + * <li>Disables selection + * <li>Disables Cut/Copy/Paste + * <li>Force-disables suggestions + * </ul> + */ +public class CarEditText extends EditText implements CarEditable { + private static final int[] ERROR_STATE = + new int[] {R.attr.state_error}; + private static final boolean SELECTION_CLAMPING_ENABLED = false; + + private int mLastSelEnd = 0; + private int mLastSelStart = 0; + private boolean mCursorClamped; + private boolean mInErrorState; + + private CarEditableListener mCarEditableListener; + private KeyListener mListener; + private InputManager mInputManager; + + /** + * Listener for events when the user interacts with the keyboard similar to {@link + * android.text.method.KeyListener}. + */ + public interface KeyListener { + /** Callback when a key is pressed. */ + void onKeyDown(char key); + + /** Callback when a key is released. */ + void onKeyUp(char key); + + /** Callback when text has been changed by another input connection or copy/paste. */ + void onCommitText(String input); + + /** Callback when the user closes the keyboard. */ + void onCloseKeyboard(); + + /** Callback when the text field has been cleared. */ + void onDelete(); + } + + @SuppressLint("ClickableViewAccessibility") + @SuppressWarnings("nullness") // suppress under initialization warning for this + public CarEditText(Context context, AttributeSet attrs) { + super(context, attrs); + setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + setTextIsSelectable(false); + setLongClickable(false); + setFocusableInTouchMode(true); + setSelection(getText().length()); + mCursorClamped = true; + setOnEditorActionListener( + new OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (mListener != null && actionId == EditorInfo.IME_ACTION_DONE) { + mListener.onCloseKeyboard(); + } + // Return false because we don't want to hijack the default behavior. + return false; + } + }); + setCustomSelectionActionModeCallback( + new Callback() { + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) {} + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return false; + } + }); + setOnTouchListener( + (v, event) -> { + if (MotionEvent.ACTION_UP == event.getAction()) { + mInputManager.startInput(CarEditText.this); + } + return false; + }); + } + + public void setKeyListener(KeyListener listener) { + mListener = listener; + } + + public void setInputManager(InputManager inputManager) { + mInputManager = inputManager; + } + + /** Sets whether this edit box is in error state or not */ + public void setErrorState(boolean inErrorState) { + mInErrorState = inErrorState; + refreshDrawableState(); + } + + @Override + protected void onSelectionChanged(int selStart, int selEnd) { + super.onSelectionChanged(selStart, selEnd); + if (mCursorClamped && SELECTION_CLAMPING_ENABLED) { + setSelection(mLastSelStart, mLastSelEnd); + return; + } + if (mCarEditableListener != null) { + mCarEditableListener.onUpdateSelection(mLastSelStart, mLastSelEnd, selStart, selEnd); + } + mLastSelStart = selStart; + mLastSelEnd = selEnd; + } + + @Override + @Nullable + public ActionMode startActionMode(Callback callback) { + return null; + } + + @Override + protected int[] onCreateDrawableState(int extraSpace) { + final int[] state; + if (mInErrorState) { + state = super.onCreateDrawableState(extraSpace + 1); + mergeDrawableStates(state, ERROR_STATE); + } else { + state = super.onCreateDrawableState(extraSpace); + } + return state; + } + + @Override + public void setCarEditableListener(CarEditableListener listener) { + mCarEditableListener = listener; + } + + @Override + public void setInputEnabled(boolean enabled) { + mCursorClamped = !enabled; + } + + @Override + public boolean performClick() { + boolean result = super.performClick(); + mInputManager.startInput(CarEditText.this); + return result; + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + InputConnection inputConnection = super.onCreateInputConnection(outAttrs); + return new InputConnectionWrapper(inputConnection, false) { + @Override + public boolean sendKeyEvent(KeyEvent event) { + // TODO(b/208707793): Remove the handleKeyEventNoWindowFocus if found system side fix for R + if (Build.VERSION.SDK_INT == VERSION_CODES.R) { + return handleKeyEventNoWindowFocus(event); + } + if (mListener != null) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + mListener.onKeyDown((char) event.getKeyCode()); + } else if (event.getAction() == KeyEvent.ACTION_UP) { + mListener.onKeyUp((char) event.getKeyCode()); + } + return true; + } else { + return super.sendKeyEvent(event); + } + } + + private boolean handleKeyEventNoWindowFocus(KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_UP) { + if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { + return super.deleteSurroundingText(1, 0); + } else { + return super.commitText(Character.toString(event.getNumber()), 1); + } + } + return false; + } + + @Override + public boolean commitText(CharSequence charSequence, int i) { + if (mListener != null) { + mListener.onCommitText(charSequence.toString()); + return true; + } + return super.commitText(charSequence, i); + } + + @Override + public boolean deleteSurroundingText(int i, int i1) { + if (mListener != null) { + mListener.onDelete(); + return true; + } + return super.deleteSurroundingText(i, i1); + } + }; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarEditTextWrapper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarEditTextWrapper.java new file mode 100644 index 0000000..1674db7 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarEditTextWrapper.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import android.annotation.SuppressLint; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.AccessibilityDelegate; +import android.view.accessibility.AccessibilityEvent; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.widget.EditText; +import androidx.annotation.Nullable; +import com.android.car.libraries.apphost.input.CarEditable; +import com.android.car.libraries.apphost.input.CarEditableListener; +import com.android.car.libraries.apphost.input.InputManager; + +/** A wrapper for {@link EditText} to make it conform to {@link CarEditable}. */ +public class CarEditTextWrapper implements CarEditable { + private final EditText mEditText; + private int mLastSelectionEnd = 0; + private int mLastSelectionStart = 0; + @Nullable private CarEditableListener mCarEditableListener; + + @SuppressLint("ClickableViewAccessibility") + @SuppressWarnings("nullness:argument") // Accessing "this" inside click listener. + public CarEditTextWrapper(EditText editText, InputManager inputManager) { + mEditText = editText; + + // Setup an accessibility delegate to get the text selection changes. This is required in + // order + // to conform to the CarEditable which requires a text selection update listener. + AccessibilityDelegate accessibilityDelegate = + new AccessibilityDelegate() { + @Override + public void sendAccessibilityEvent(View host, int eventType) { + super.sendAccessibilityEvent(host, eventType); + if (eventType == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) { + if (mCarEditableListener != null) { + mCarEditableListener.onUpdateSelection( + mLastSelectionStart, + mLastSelectionEnd, + mEditText.getSelectionStart(), + mEditText.getSelectionEnd()); + } + mLastSelectionStart = mEditText.getSelectionStart(); + mLastSelectionEnd = mEditText.getSelectionEnd(); + } + } + }; + editText.setAccessibilityDelegate(accessibilityDelegate); + editText.setOnClickListener((view) -> inputManager.startInput(CarEditTextWrapper.this)); + editText.setOnFocusChangeListener( + (view, hasFocus) -> { + if (!hasFocus) { + inputManager.stopInput(); + } + }); + // Android will dispatch in the following order: + // onTouch + // onFocus + // onClick + // However if internally the view consumes any it will stop dispatching. If an EditText + // does not have focus it will consume the focus and not send the onClick. + editText.setOnTouchListener( + (v, event) -> { + if (MotionEvent.ACTION_UP == event.getAction()) { + inputManager.startInput(CarEditTextWrapper.this); + } + return false; + }); + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + return mEditText.onCreateInputConnection(outAttrs); + } + + @Override + public void setCarEditableListener(@Nullable CarEditableListener listener) { + mCarEditableListener = listener; + } + + @Override + public void setInputEnabled(boolean enabled) { + mEditText.setEnabled(enabled); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarImageView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarImageView.java new file mode 100644 index 0000000..0e3d108 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarImageView.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.widget.ImageView; +import androidx.annotation.Nullable; +import com.android.car.libraries.templates.host.R; + +/** An {@link ImageView} that enforces size limits on OEM-customized width and height. */ +@SuppressLint("AppCompatCustomView") +public final class CarImageView extends ImageView { + private final int mMinWidth; + private final int mMaxWidth; + private final int mMinHeight; + private final int mMaxHeight; + + public CarImageView(Context context) { + this(context, null); + } + + public CarImageView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public CarImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public CarImageView( + Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + TypedArray ta = + context.obtainStyledAttributes(attrs, R.styleable.CarImageView, defStyleAttr, 0); + mMinWidth = ta.getDimensionPixelSize(R.styleable.CarImageView_imageMinWidth, 0); + mMaxWidth = ta.getDimensionPixelSize(R.styleable.CarImageView_imageMaxWidth, Integer.MAX_VALUE); + mMinHeight = ta.getDimensionPixelSize(R.styleable.CarImageView_imageMinHeight, 0); + mMaxHeight = + ta.getDimensionPixelSize(R.styleable.CarImageView_imageMaxHeight, Integer.MAX_VALUE); + ta.recycle(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Set the OEM-customizable image size, with the min and max limits. + ViewUtils.enforceViewSizeLimit(this, mMinWidth, mMaxWidth, mMinHeight, mMaxHeight); + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarProgressBar.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarProgressBar.java new file mode 100644 index 0000000..8a9fd75 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarProgressBar.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.widget.ProgressBar; +import androidx.annotation.Nullable; +import com.android.car.libraries.templates.host.R; + +/** An {@link ProgressBar} that enforces size limits on OEM-customized width and height. */ +public class CarProgressBar extends ProgressBar { + private final int mMinSize; + private final int mMaxSize; + + public CarProgressBar(Context context) { + this(context, null); + } + + public CarProgressBar(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public CarProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public CarProgressBar( + Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + TypedArray ta = + context.obtainStyledAttributes(attrs, R.styleable.CarProgressBar, defStyleAttr, 0); + mMinSize = ta.getDimensionPixelSize(R.styleable.CarProgressBar_imageMinSize, 0); + mMaxSize = ta.getDimensionPixelSize(R.styleable.CarProgressBar_imageMaxSize, Integer.MAX_VALUE); + ta.recycle(); + } + + @Override + protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Set the OEM-customizable image size, with the min and max limits. + ViewUtils.enforceViewSizeLimit(this, mMinSize, mMaxSize, mMinSize, mMaxSize); + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarUiTextUtils.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarUiTextUtils.java new file mode 100644 index 0000000..7a92013 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarUiTextUtils.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import static java.util.Objects.requireNonNull; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.car.app.model.CarText; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.view.common.CarTextParams; +import com.android.car.libraries.apphost.view.common.CarTextUtils; +import com.android.car.ui.CarUiText; +import java.util.ArrayList; +import java.util.List; + +/** Util class for {@link CarUiText}. */ +public class CarUiTextUtils { + private CarUiTextUtils() {} + + /** Creates a {@link CarUiText} from a {@link CarText}. */ + public static CarUiText fromCarText( + TemplateContext context, @Nullable CarText carText, int maxLines) { + return fromCarText(context, carText, CarTextParams.DEFAULT, maxLines); + } + + /** Creates a {@link CarUiText} from a {@link CarText}. */ + public static CarUiText fromCarText( + TemplateContext context, @Nullable CarText carText, CarTextParams params, int maxLines) { + if (CarText.isNullOrEmpty(carText)) { + return new CarUiText("", maxLines); + } + requireNonNull(carText); + + List<CharSequence> textVariants = new ArrayList<>(); + textVariants.add(CarTextUtils.toCharSequenceOrEmpty(context, carText, params)); + for (int i = 0; i < carText.getVariants().size(); i++) { + textVariants.add(CarTextUtils.toCharSequenceOrEmpty(context, carText, params, i)); + } + return new CarUiText.Builder(textVariants) + .setMaxLines(maxLines) + .setMaxChars(context.getConstraintsProvider().getStringCharacterLimit()) + .build(); + } + + /** Creates a {@link CarUiText} from a {@link CharSequence}. */ + public static CarUiText fromCharSequence( + TemplateContext context, @NonNull CharSequence charSequence, int maxLines) { + + return new CarUiText.Builder(charSequence) + .setMaxLines(maxLines) + .setMaxChars(context.getConstraintsProvider().getStringCharacterLimit()) + .build(); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CardHeaderView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CardHeaderView.java new file mode 100644 index 0000000..d3c1662 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CardHeaderView.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; +import androidx.annotation.StyleableRes; +import androidx.car.app.model.Action; +import androidx.car.app.model.CarIcon; +import androidx.car.app.model.CarText; +import androidx.car.app.model.OnClickDelegate; +import androidx.car.app.model.OnContentRefreshDelegate; +import androidx.core.graphics.drawable.IconCompat; +import com.android.car.libraries.apphost.common.CommonUtils; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.distraction.TemplateValidator; +import com.android.car.libraries.apphost.view.common.ImageUtils; +import com.android.car.libraries.apphost.view.common.ImageViewParams; +import com.android.car.libraries.templates.host.R; +import com.android.car.ui.widget.CarUiTextView; + +/** A view that displays the header for the templates. */ +public class CardHeaderView extends LinearLayout { + private CarUiTextView mHeaderTitle; + private ImageView mHeaderButtonIcon; + private FrameLayout mHeaderButtonContainer; + private FrameLayout mRefreshButtonContainer; + private ImageView mRefreshButtonIcon; + @ColorInt private final int mHeaderIconTint; + + public CardHeaderView(Context context) { + this(context, null); + } + + public CardHeaderView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public CardHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + @SuppressWarnings("nullness:argument") // Fix UnderInitialization warnings + public CardHeaderView( + Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + LayoutInflater.from(context).inflate(R.layout.header_view, this); + + @StyleableRes final int[] themeAttrs = {R.attr.templateHeaderButtonIconTint}; + TypedArray themeAttrsArray = context.obtainStyledAttributes(themeAttrs); + mHeaderIconTint = themeAttrsArray.getColor(0, 0); + themeAttrsArray.recycle(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mHeaderTitle = findViewById(R.id.header_title); + mHeaderButtonContainer = findViewById(R.id.header_button_container); + mHeaderButtonIcon = findViewById(R.id.header_icon); + mRefreshButtonContainer = findViewById(R.id.refresh_button_container); + mRefreshButtonIcon = findViewById(R.id.refresh_icon); + ViewUtils.setMinTapTarget( + this, + mHeaderButtonContainer, + getResources().getDimensionPixelSize(R.dimen.template_min_tap_target_size)); + } + + /** + * Update the {@link HeaderView} to show the given {@code title} and header {@code action}. + * + * <p>If the inputs are {@code null} then the view is hidden. + */ + public void setContent( + TemplateContext templateContext, @Nullable CarText title, @Nullable Action action) { + setContent(templateContext, title, action, null); + } + + /** + * Update the {@link HeaderView} to show the given {@code title}, header {@code action}, and, if + * {@code contentRefreshDelegate} is not {@code null}, a refresh button that allow users to + * interact with to trigger refreshes. + * + * <p>If the inputs are {@code null} then the view is hidden. + */ + public void setContent( + TemplateContext templateContext, + @Nullable CarText title, + @Nullable Action action, + @Nullable OnContentRefreshDelegate contentRefreshDelegate) { + boolean isVisible = title != null; + if (isVisible) { + mHeaderTitle.setText( + CarUiTextUtils.fromCarText(templateContext, title, mHeaderTitle.getMaxLines())); + + mHeaderTitle.setVisibility(VISIBLE); + } else { + mHeaderTitle.setVisibility(GONE); + } + + isVisible |= updateHeaderButton(templateContext, action); + isVisible |= updateRefreshButton(templateContext, contentRefreshDelegate); + setVisibility(isVisible ? VISIBLE : GONE); + } + + /** + * Updates the optional button in the header. + * + * @return true if the button ended up visible, false otherwise. + */ + private boolean updateHeaderButton(TemplateContext templateContext, @Nullable Action action) { + if (action == null) { + mHeaderButtonContainer.setVisibility(GONE); + return false; + } + + mHeaderButtonContainer.setVisibility(VISIBLE); + + ImageUtils.setImageSrc( + templateContext, + ImageUtils.getIconFromAction(action), + mHeaderButtonIcon, + ImageViewParams.builder().setDefaultTint(mHeaderIconTint).setForceTinting(true).build()); + + if (action.getType() == Action.TYPE_APP_ICON) { + // Special treatment for app icon as it is un-clickable and un-focusable. + mHeaderButtonContainer.setFocusable(false); + mHeaderButtonContainer.setClickable(false); + } else if (action.getType() == Action.TYPE_BACK) { + // Special treatment for back as it doesn't have a custom click listener + mHeaderButtonContainer.setOnClickListener( + view -> templateContext.getBackPressedHandler().onBackPressed()); + mHeaderButtonContainer.setFocusable(true); + mHeaderButtonContainer.setClickable(true); + } else { + OnClickDelegate onClickDelegate = action.getOnClickDelegate(); + if (onClickDelegate != null) { + mHeaderButtonContainer.setOnClickListener( + view -> CommonUtils.dispatchClick(templateContext, onClickDelegate)); + mHeaderButtonContainer.setFocusable(true); + mHeaderButtonContainer.setClickable(true); + } else { + mHeaderButtonContainer.setFocusable(false); + mHeaderButtonContainer.setClickable(false); + } + } + + return true; + } + + private boolean updateRefreshButton( + TemplateContext templateContext, @Nullable OnContentRefreshDelegate contentRefreshDelegate) { + if (!templateContext.getCarHostConfig().isPoiContentRefreshEnabled() + || contentRefreshDelegate == null) { + mRefreshButtonContainer.setVisibility(GONE); + mRefreshButtonContainer.setFocusable(false); + mRefreshButtonContainer.setClickable(false); + return false; + } + + CarIcon icon = + new CarIcon.Builder( + IconCompat.createWithResource( + getContext(), templateContext.getHostResourceIds().getRefreshIconDrawable())) + .build(); + ImageUtils.setImageSrc( + templateContext, + icon, + mRefreshButtonIcon, + ImageViewParams.builder().setDefaultTint(mHeaderIconTint).setForceTinting(true).build()); + + mRefreshButtonContainer.setVisibility(VISIBLE); + mRefreshButtonContainer.setFocusable(true); + mRefreshButtonContainer.setClickable(true); + mRefreshButtonContainer.setOnClickListener( + view -> { + TemplateValidator templateValidator = + templateContext.getAppHostService(TemplateValidator.class); + if (templateValidator != null) { + templateValidator.setIsNextTemplateContentRefreshIfSameType(true); + } + templateContext.getAppDispatcher().dispatchContentRefreshRequest(contentRefreshDelegate); + }); + + return true; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ClickableSpanTextContainer.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ClickableSpanTextContainer.java new file mode 100644 index 0000000..a5db03b --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ClickableSpanTextContainer.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.text.Selection; +import android.text.Spannable; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.text.style.BackgroundColorSpan; +import android.text.style.ClickableSpan; +import android.text.style.ForegroundColorSpan; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewTreeObserver.OnGlobalFocusChangeListener; +import android.widget.FrameLayout; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StyleableRes; +import com.android.car.libraries.templates.host.R; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A container for a {@link TextView} that allows moving focus between clickable spans. + * + * <p>Only the vertical focus movement are supported. + */ +public class ClickableSpanTextContainer extends FrameLayout implements OnGlobalFocusChangeListener { + /** An invalid span index. */ + private static final int INVALID_INDEX = -1; + + private final ForegroundColorSpan mHighlightForegroundSpan; + private final BackgroundColorSpan mHighlightBackgroundSpan; + + private final List<ClickableSpan> mClickableSpans = new ArrayList<>(); + private int mSelectedSpanIndex = INVALID_INDEX; + + /** Indicates whether the focus moved in between spans. */ + private boolean mMovedClickableSpanFocus = false; + + private TextView mClickableSpanCarTextView; + + public ClickableSpanTextContainer(@NonNull Context context) { + this(context, null); + } + + public ClickableSpanTextContainer(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public ClickableSpanTextContainer( + @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ClickableSpanTextContainer( + @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + @StyleableRes + final int[] themeAttrs = { + R.attr.templateClickableSpanHighlightForegroundColor, + R.attr.templateClickableSpanHighlightBackgroundColor, + }; + + TypedArray ta = context.obtainStyledAttributes(themeAttrs); + int highlightForegroundColor = ta.getColor(0, Color.TRANSPARENT); + int highlightBackgroundColor = ta.getColor(1, Color.TRANSPARENT); + ta.recycle(); + + mHighlightForegroundSpan = new ForegroundColorSpan(highlightForegroundColor); + mHighlightBackgroundSpan = new BackgroundColorSpan(highlightBackgroundColor); + } + + /** Sets the given text for the wrapped text view. */ + public void setText(@Nullable CharSequence text) { + mClickableSpanCarTextView.setText(text); + + mClickableSpans.clear(); + if (text instanceof Spannable) { + Spannable spannable = (Spannable) text; + Collections.addAll( + mClickableSpans, spannable.getSpans(0, text.length(), ClickableSpan.class)); + } + } + + @Override + public void onGlobalFocusChanged(View oldFocus, View newFocus) { + if (newFocus == null) { + // The focus left the window, remove the link highlight. + removeLinkHighlight(); + return; + } + + if (oldFocus == null) { + // The focus came back to the window, show the link highlight again if applicable. + updateSelectedSpan(); + return; + } + + int focusDirection = focusDirection(oldFocus, newFocus); + if (newFocus.equals(mClickableSpanCarTextView)) { + // The focus moved from another view to this view. Determine which clickable is + // selected. + if (mMovedClickableSpanFocus) { + // The user moved focus, but we brought the focus back to this view after changing + // the + // selected clickable span index to simulate the clickable span focus movement. Do + // not reset + // the index. + mMovedClickableSpanFocus = false; + } else { + if (focusDirection == FOCUS_UP) { + // The focus moved up. Select the last span in the list. + mSelectedSpanIndex = + mClickableSpans.isEmpty() ? INVALID_INDEX : mClickableSpans.size() - 1; + } else { + // The focus moved down. Select the first span in the list. + mSelectedSpanIndex = mClickableSpans.isEmpty() ? INVALID_INDEX : 0; + } + } + + updateSelectedSpan(); + } else if (oldFocus.equals(mClickableSpanCarTextView)) { + // The focus moved from this view to another view. + if (mSelectedSpanIndex != INVALID_INDEX) { + if (focusDirection == FOCUS_UP && mSelectedSpanIndex > 0) { + // Focus moved up within the span list, select an earlier span and focus on the + // text view + // again. + mSelectedSpanIndex--; + mMovedClickableSpanFocus = true; + mClickableSpanCarTextView.requestFocus(); + } else if (focusDirection == FOCUS_DOWN + && mSelectedSpanIndex < mClickableSpans.size() - 1) { + // Focus moved down within the span list, select a later span and focus on the + // text view + // again. + mSelectedSpanIndex++; + mMovedClickableSpanFocus = true; + mClickableSpanCarTextView.requestFocus(); + } else { + // Focus moved out of the span list, remove the selected span. + mSelectedSpanIndex = INVALID_INDEX; + updateSelectedSpan(); + } + } + } + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mClickableSpanCarTextView = findViewById(R.id.clickable_span_text_view); + + // Enable clickable spans here, setting these in the resourc1e file does not work + mClickableSpanCarTextView.setMovementMethod(LinkMovementMethod.getInstance()); + mClickableSpanCarTextView.setHighlightColor(Color.TRANSPARENT); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + getViewTreeObserver().addOnGlobalFocusChangeListener(this); + } + + @Override + protected void onDetachedFromWindow() { + getViewTreeObserver().removeOnGlobalFocusChangeListener(this); + + super.onDetachedFromWindow(); + } + + /** Updates the selected clickable span. */ + private void updateSelectedSpan() { + Spannable spannable = (Spannable) mClickableSpanCarTextView.getText(); + if (spannable == null) { + return; + } + + if (mSelectedSpanIndex == INVALID_INDEX) { + Selection.removeSelection(spannable); + spannable.removeSpan(mHighlightForegroundSpan); + spannable.removeSpan(mHighlightBackgroundSpan); + } else { + // highlight the selected span. + ClickableSpan span = mClickableSpans.get(mSelectedSpanIndex); + int spanStart = spannable.getSpanStart(span); + int spanEnd = spannable.getSpanEnd(span); + Selection.setSelection(spannable, spanStart, spanEnd); + + spannable.setSpan( + mHighlightForegroundSpan, spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + mHighlightBackgroundSpan, spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + /** + * Removes the link highlight from the selected span. + * + * <p>This method only removes the visual highlight, but not the selected span. + */ + private void removeLinkHighlight() { + Spannable spannable = (Spannable) mClickableSpanCarTextView.getText(); + if (spannable != null) { + spannable.removeSpan(mHighlightForegroundSpan); + spannable.removeSpan(mHighlightBackgroundSpan); + } + } + + /** + * Determines which direction the focus moved from the old to new focus. + * + * <p>This method only determines the vertical focus direction. + */ + private static int focusDirection(View oldFocus, View newFocus) { + int[] oldLocation = getViewLocationInWindow(oldFocus); + int[] newLocation = getViewLocationInWindow(newFocus); + int oldLocationY = oldLocation[1]; + int newLocationY = newLocation[1]; + return oldLocationY > newLocationY ? FOCUS_UP : FOCUS_DOWN; + } + + private static int[] getViewLocationInWindow(@Nullable View view) { + int[] location = new int[2]; + if (view != null) { + view.getLocationInWindow(location); + } + return location; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ContentView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ContentView.java new file mode 100644 index 0000000..0e731e7 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ContentView.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import static com.android.car.libraries.apphost.template.view.model.RowListWrapper.LIST_FLAGS_HIDE_ROW_DIVIDERS; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import androidx.annotation.Nullable; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.template.view.model.RowListWrapper; +import com.android.car.libraries.templates.host.R; + +/** A view that displays content such as a list, a pane, or an error screen. */ +public class ContentView extends LinearLayout { + private ViewGroup mViewGroup; + + public ContentView(Context context) { + this(context, null); + } + + public ContentView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public ContentView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + @SuppressWarnings({"ResourceType", "method.invocation.invalid", "argument.type.incompatible"}) + public ContentView( + Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mViewGroup = findViewById(R.id.container); + } + + /** Sets a {@link GridWrapper} as the content for this view. */ + public void setGridContent(TemplateContext templateContext, GridWrapper gridWrapper) { + View view = mViewGroup.getChildCount() > 0 ? mViewGroup.getChildAt(0) : null; + if (view != null) { + if (!(view instanceof GridView)) { + removeView(view); + view = null; + } + } + + if (view == null) { + view = LayoutInflater.from(getContext()).inflate(R.layout.grid_view, mViewGroup, false); + mViewGroup.addView(view); + } + + ((GridView) view).setGrid(templateContext, gridWrapper); + } + + /** Sets a {@link RowListWrapper} as the content for this view. */ + public void setRowListContent(TemplateContext templateContext, RowListWrapper rowList) { + View view = mViewGroup.getChildCount() > 0 ? mViewGroup.getChildAt(0) : null; + if (view != null) { + if (!(view instanceof RowListView)) { + removeView(view); + view = null; + } + } + + if (view == null) { + boolean hasRowDividers = (rowList.getListFlags() & LIST_FLAGS_HIDE_ROW_DIVIDERS) == 0; + int layout = + rowList.isHalfList() + ? R.layout.half_list_view + : hasRowDividers ? R.layout.full_list_view : R.layout.full_list_no_divider_view; + view = LayoutInflater.from(getContext()).inflate(layout, mViewGroup, false); + mViewGroup.addView(view); + } + + ((RowListView) view).setRowList(templateContext, rowList); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/FabView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/FabView.java new file mode 100644 index 0000000..23b4211 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/FabView.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import static androidx.car.app.model.Action.TYPE_BACK; + +import android.content.Context; +import android.content.res.TypedArray; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.ImageView; +import android.widget.LinearLayout; +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; +import androidx.annotation.StyleableRes; +import androidx.annotation.VisibleForTesting; +import androidx.car.app.model.Action; +import androidx.car.app.model.CarIcon; +import androidx.car.app.model.OnClickDelegate; +import com.android.car.libraries.apphost.common.CommonUtils; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction; +import com.android.car.libraries.apphost.template.view.model.ActionWrapper; +import com.android.car.libraries.apphost.view.common.CarTextUtils; +import com.android.car.libraries.apphost.view.common.ImageUtils; +import com.android.car.libraries.apphost.view.common.ImageViewParams; +import com.android.car.libraries.templates.host.R; +import com.android.car.ui.widget.CarUiTextView; + +/** Displays an {@link Action} as a FAB. */ +// TODO(b/158142806): Merge with ActionButtonView +public class FabView extends LinearLayout { + private Object mAction; + private final int mMinWidthWithText; + private final int mMinWidthWithoutText; + @ColorInt private final int mContentColor; + @ColorInt private final int mBackgroundColorLight; + @ColorInt private final int mBackgroundColorDark; + + public FabView(Context context) { + this(context, null); + } + + public FabView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public FabView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public FabView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + @StyleableRes + final int[] themeAttrs = { + R.attr.templateActionWithTextMinWidth, + R.attr.templateActionWithoutTextMinWidth, + R.attr.templateActionStripFabBackgroundColorLight, + R.attr.templateActionStripFabBackgroundColorDark + }; + + TypedArray ta = context.obtainStyledAttributes(themeAttrs); + mMinWidthWithText = ta.getDimensionPixelSize(0, 0); + mMinWidthWithoutText = ta.getDimensionPixelSize(1, 0); + mBackgroundColorLight = ta.getColor(2, 0); + mBackgroundColorDark = ta.getColor(3, 0); + ta.recycle(); + + ta = context.obtainStyledAttributes(defStyleRes, new int[] {R.attr.fabDefaultContentColor}); + mContentColor = ta.getColor(0, -1); + ta.recycle(); + } + + /** Returns whether the button contains a text or not. */ + public boolean hasTitle() { + CarUiTextView textView = findViewById(R.id.action_text); + return textView != null && !TextUtils.isEmpty(textView.getText()); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + } + + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) + public Object getAction() { + return mAction; + } + + /** Updates the view from based on the input {@code action}. */ + public void setAction(TemplateContext templateContext, ActionWrapper actionWrapper) { + removeAllViews(); + Action action = actionWrapper.get(); + mAction = action; + + CharSequence title = CarTextUtils.toCharSequenceOrEmpty(templateContext, action.getTitle()); + CarIcon icon = ImageUtils.getIconFromAction(action); + + LayoutInflater inflater = LayoutInflater.from(getContext()); + + boolean hasTitle = title.length() > 0; + setMinimumWidth(hasTitle ? mMinWidthWithText : mMinWidthWithoutText); + + if (icon != null && hasTitle) { + inflater.inflate(R.layout.fab_view_icon_text, this); + } else if (hasTitle) { + inflater.inflate(R.layout.fab_view_text, this); + } else { + inflater.inflate(R.layout.action_button_view_icon, this); + } + + if (icon != null) { + @ColorInt + int backgroundColor = + CommonUtils.isDarkMode(templateContext) ? mBackgroundColorDark : mBackgroundColorLight; + ImageViewParams imageViewParams = + ImageViewParams.builder() + .setDefaultTint(mContentColor) + .setForceTinting(true) + .setBackgroundColor(backgroundColor) + .build(); + + ImageView iconView = findViewById(R.id.action_icon); + ImageUtils.setImageSrc(templateContext, icon, iconView, imageViewParams); + } + + // Add the text view. + if (hasTitle) { + CarUiTextView carUiTextView = findViewById(R.id.action_text); + carUiTextView.setText( + CarUiTextUtils.fromCarText( + templateContext, action.getTitle(), carUiTextView.getMaxLines())); + carUiTextView.setTextColor(mContentColor); + } + + // Update the click listener, if one is set. + if (action.getType() == TYPE_BACK) { + setOnClickListener(v -> templateContext.getBackPressedHandler().onBackPressed()); + } else { + OnClickDelegate onClickDelegate = action.getOnClickDelegate(); + ActionWrapper.OnClickListener hostListener = actionWrapper.getOnClickListener(); + if (onClickDelegate != null || hostListener != null) { + setOnClickListener( + v -> { + if (hostListener != null) { + hostListener.onClick(); + } + + ViewUtils.logCarAppTelemetry(templateContext, UiAction.ACTION_STRIP_FAB_CLICKED); + if (onClickDelegate != null) { + CommonUtils.dispatchClick(templateContext, onClickDelegate); + } + }); + } else { + setOnClickListener(null); + } + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridAdapter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridAdapter.java new file mode 100644 index 0000000..365faae --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridAdapter.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import static java.lang.Math.min; + +import android.content.Context; + +import android.view.LayoutInflater; +import android.view.ViewGroup; +import androidx.annotation.VisibleForTesting; +import androidx.car.app.constraints.ConstraintManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.templates.host.R; +import com.android.car.ui.recyclerview.CarUiRecyclerView; +import com.google.common.collect.ImmutableList; +import java.util.List; + +/** A grid adapter for {@link GridItemWrapper}s. */ +@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) +public class GridAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> + implements CarUiRecyclerView.ItemCap { + + private final Context mContext; + private final int mItemsPerRow; + private List<GridRowWrapper> mRowWrappers; + private List<GridItemWrapper> mItemWrappers; + + private TemplateContext mTemplateContext; + private int mMaxItemCount; + + static GridAdapter create(Context context, int itemsPerRow) { + return new GridAdapter(context, itemsPerRow); + } + + void setGridItems(TemplateContext templateContext, List<GridItemWrapper> gridItemWrappers) { + mTemplateContext = templateContext; + mMaxItemCount = + mTemplateContext + .getConstraintsProvider() + .getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_GRID); + mItemWrappers = gridItemWrappers; + mRowWrappers = GridRowWrapper.create(gridItemWrappers, mItemsPerRow); + + notifyDataSetChanged(); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { + return new RecyclerView.ViewHolder( + LayoutInflater.from(mContext).inflate(R.layout.grid_item_view, viewGroup, false)) {}; + } + + @Override + public void onBindViewHolder(ViewHolder viewHolder, int index) { + GridItemWrapper gridItemWrapper = mItemWrappers.get(index); + GridRowWrapper gridRowWrapper = findGridRowWrapperForItemAt(index); + ((GridItemView) viewHolder.itemView) + .setGridItem( + mTemplateContext, + gridItemWrapper, + gridRowWrapper.hasGridItemsWithTitle(), + gridRowWrapper.hasGridItemsWithText()); + } + + @Override + public int getItemCount() { + if (mMaxItemCount == CarUiRecyclerView.ItemCap.UNLIMITED) { + return mItemWrappers.size(); + } else { + return min(mItemWrappers.size(), mMaxItemCount); + } + } + + @Override + public void setMaxItems(int maxItems) { + TemplateContext templateContext = mTemplateContext; + if (templateContext == null) { + return; + } + + int gridMaxLength = + templateContext + .getConstraintsProvider() + .getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_GRID); + if (maxItems == CarUiRecyclerView.ItemCap.UNLIMITED) { + mMaxItemCount = gridMaxLength; + } else { + mMaxItemCount = min(maxItems, gridMaxLength); + } + } + + @VisibleForTesting + public List<GridRowWrapper> getRowWrappers() { + return mRowWrappers; + } + + @VisibleForTesting + public List<GridItemWrapper> getItemWrappers() { + return mItemWrappers; + } + + /** Returns the {@link GridRowWrapper} associated with the item at given index. */ + private GridRowWrapper findGridRowWrapperForItemAt(int index) { + int currentIndex = index; + for (GridRowWrapper gridRowWrapper : mRowWrappers) { + int rowItemsCount = gridRowWrapper.getGridRowItems().size(); + if (currentIndex < rowItemsCount) { + return gridRowWrapper; + } + currentIndex -= rowItemsCount; + } + + throw new IndexOutOfBoundsException( + String.format("index = %d >= %d = count", index, getItemCount())); + } + + private GridAdapter(Context context, int itemsPerRow) { + mContext = context; + mItemWrappers = ImmutableList.of(); + mRowWrappers = ImmutableList.of(); + mItemsPerRow = itemsPerRow; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridItemView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridItemView.java new file mode 100644 index 0000000..d03f278 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridItemView.java @@ -0,0 +1,310 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; +import androidx.annotation.StyleableRes; +import androidx.car.app.model.CarColor; +import androidx.car.app.model.CarIcon; +import androidx.car.app.model.CarText; +import androidx.car.app.model.GridItem; +import androidx.car.app.model.OnClickDelegate; +import com.android.car.libraries.apphost.common.CarColorUtils; +import com.android.car.libraries.apphost.common.CommonUtils; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.distraction.constraints.CarColorConstraints; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.template.view.model.SelectionGroup; +import com.android.car.libraries.apphost.view.common.CarTextParams; +import com.android.car.libraries.apphost.view.common.CarTextUtils; +import com.android.car.libraries.apphost.view.common.ImageUtils; +import com.android.car.libraries.apphost.view.common.ImageViewParams; +import com.android.car.libraries.templates.host.R; +import com.android.car.ui.widget.CarUiTextView; + +/** A view that can display a {@link GridItem} model. */ +public class GridItemView extends LinearLayout { + private static final int[] STATE_INACTIVE_FOCUS = {R.attr.templateFocusStateInactive}; + + /** Text parameters for secondary text in a grid item. */ + private static final CarTextParams TEXT_PARAMS_SECONDARY_TEXT = + CarTextParams.builder() + .setColorSpanConstraints(CarColorConstraints.STANDARD_ONLY) + .setMaxImages(0) + .build(); + /** + * Indicates whether or not this grid item has inactive focus. + * + * <p>The grid item has an inactive focus when it is not clickable. + */ + private boolean mHasInactiveFocus; + + private final int mLargeImageSizeMin; + private final int mLargeImageSizeMax; + @ColorInt private final int mDefaultIconTint; + @ColorInt private final int mBackgroundColor; + private final int mHorizontalTextBottomPadding; + private final Drawable mGridItemBackground; + + private LinearLayout mImageContainer; + private LinearLayout mTextContainer; + private CarUiTextView mTitleView; + private CarUiTextView mTextview; + private ImageView mImageView; + private ProgressBar mProgressBar; + private int mTextTopPadding; + private int mTextBottomPadding; + + public GridItemView(Context context) { + this(context, null); + } + + public GridItemView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + @SuppressWarnings("nullness:assignment") + public GridItemView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + @StyleableRes + final int[] themeAttrs = { + R.attr.templateLargeImageSizeMin, + R.attr.templateLargeImageSizeMax, + R.attr.templateGridItemDefaultIconTint, + R.attr.templateGridItemTextBottomPadding, + R.attr.templateGridItemBackground, + R.attr.templateGridItemBackgroundColor, + }; + + TypedArray ta = context.obtainStyledAttributes(themeAttrs); + mLargeImageSizeMin = ta.getDimensionPixelSize(0, 0); + mLargeImageSizeMax = ta.getDimensionPixelSize(1, Integer.MAX_VALUE); + mDefaultIconTint = ta.getColor(2, 0); + mHorizontalTextBottomPadding = ta.getDimensionPixelSize(3, 0); + mGridItemBackground = ta.getDrawable(4); + mBackgroundColor = ta.getColor(5, 0); + ta.recycle(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mImageContainer = findViewById(R.id.grid_item_image_container); + mTextContainer = findViewById(R.id.grid_item_text_container); + mTitleView = findViewById(R.id.grid_item_title); + mTextview = findViewById(R.id.grid_item_text); + mImageView = findViewById(R.id.grid_item_image); + mProgressBar = findViewById(R.id.grid_item_progress_bar); + + // Cache TextContainer padding since the padding is updated every time {@link #setGridItem} is + // called. + mTextTopPadding = mTextContainer.getPaddingTop(); + mTextBottomPadding = mTextContainer.getPaddingBottom(); + + ViewUtils.enforceViewSizeLimit(mImageContainer, mLargeImageSizeMin, mLargeImageSizeMax); + } + + @Override + protected int[] onCreateDrawableState(int extraSpace) { + if (mHasInactiveFocus) { + // We are going to add 1 extra state. + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + + mergeDrawableStates(drawableState, STATE_INACTIVE_FOCUS); + return drawableState; + } else { + return super.onCreateDrawableState(extraSpace); + } + } + + /** Updates the view with the given {@link GridItemWrapper}. */ + public void setGridItem( + TemplateContext templateContext, + GridItemWrapper gridItemWrapper, + boolean shouldShowTitle, + boolean shouldShowText) { + GridItem gridItem = gridItemWrapper.getGridItem(); + + L.v(LogTags.TEMPLATE, "Setting grid item view with grid item: %s", gridItem); + + // Unset any click/focus listeners tied to the previous content. New ones will be added + // below. + setOnClickListener(null); + setOnFocusChangeListener(null); + + updateTextView( + templateContext, mTitleView, gridItem.getTitle(), CarTextParams.DEFAULT, shouldShowTitle); + + // Allow standard colors for the secondary text only if the color contrast check passed. + boolean colorContrastCheckPassed = + checkColorContrast(templateContext, gridItem, mBackgroundColor); + CarTextParams secondaryTextParams = + colorContrastCheckPassed ? TEXT_PARAMS_SECONDARY_TEXT : CarTextParams.DEFAULT; + updateTextView( + templateContext, mTextview, gridItem.getText(), secondaryTextParams, shouldShowText); + + mTextContainer.setPadding( + 0, + mTextTopPadding, + 0, + + // If there is not secondary text to be shown, add an extra padding at the bottom of + // the + // container. This makes it so that there's more separation between rows when + // there's only + // a title (for example, in the system's wallpaper picker), while we use up some + // more + // of the vertical space for text when there is a secondary line. + mTextBottomPadding + (shouldShowText ? 0 : mHorizontalTextBottomPadding)); + + SelectionGroup selectionGroup = gridItemWrapper.getSelectionGroup(); + OnClickDelegate onClickDelegate = gridItem.getOnClickDelegate(); + + boolean isLoading = gridItem.isLoading(); + + // The grid item is clickable iff... + boolean isClickable = + // ...it is not in the loading state, and + !isLoading + && ( + // ...it has a click listener coming from the client + onClickDelegate != null + // ...is selectable + || selectionGroup != null); + + // Show either the image or the loading spinner. + mProgressBar.setVisibility(isLoading ? VISIBLE : GONE); + mImageView.setVisibility(isLoading ? GONE : VISIBLE); + if (!isLoading) { + int imageType = gridItem.getImageType(); + + // Show the grid item image. + CarIcon image = gridItem.getImage(); + ImageUtils.setImageSrc( + templateContext, + image, + mImageView, + ImageViewParams.builder() + .setDefaultTint(mDefaultIconTint) + .setForceTinting(imageType == GridItem.IMAGE_TYPE_ICON) + .setBackgroundColor(mBackgroundColor) + .setIgnoreAppTint(!colorContrastCheckPassed) + .build()); + + // Set the onClickListener on the grid item iff... + if (onClickDelegate != null) { + // ...it has a click listener from the client. Dispatch click event to the + // onClickListener. + setOnClickListener( + v -> { + CommonUtils.dispatchClick(templateContext, onClickDelegate); + }); + } else if (selectionGroup != null) { + // ...it is part of a selection group. Dispatch a selection change event to the + // selection + // group's onSelectedListener. + setOnClickListener( + v -> { + int currentSelectionIndex = selectionGroup.getSelectedIndex(); + int newIndex = gridItemWrapper.getGridItemIndex(); + + if (currentSelectionIndex != newIndex) { + selectionGroup.setSelectedIndex(newIndex); + } + + // Dispatch the selection callbacks. + // Note the selection event is dispatched regardless of selection index + // actually + // changing. + templateContext + .getAppDispatcher() + .dispatchSelected( + selectionGroup.getOnSelectedDelegate(), + selectionGroup.getRelativeIndex(newIndex)); + }); + } + } + + setClickable(isClickable); + setBackground(mGridItemBackground); + setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); + setInactiveFocus(!isClickable); + } + + /** Checks the color contrast between contents of the given grid item and the background color. */ + private static boolean checkColorContrast( + TemplateContext templateContext, GridItem gridItem, @ColorInt int backgroundColor) { + // Only the secondary text can be colored, so check it + CarText secondaryText = gridItem.getText(); + if (secondaryText != null) { + if (!CarTextUtils.checkColorContrast(templateContext, secondaryText, backgroundColor)) { + return false; + } + } + + CarIcon image = gridItem.getImage(); + if (image == null) { + return true; + } + CarColor tint = image.getTint(); + if (tint == null) { + return true; + } + + return CarColorUtils.checkColorContrast(templateContext, tint, backgroundColor); + } + + private static void updateTextView( + TemplateContext templateContext, + CarUiTextView carUiTextView, + @Nullable CarText text, + CarTextParams textParams, + boolean shouldShowTextView) { + // The visibility of the text view inside a grid view depends on all the grid items in the + // row. It's possible that this particular grid item doesn't have a valid title or text, but + // another grid item in the row may have a title. We need to have consistent height and + // focus states for all the grid items in a gird row. Using information provided by the grid + // row container to decide the visibility of text view's inside a grid item. + carUiTextView.setVisibility(shouldShowTextView ? VISIBLE : GONE); + + // With the "normal" buffer type, the text view sets a spanned text with immutable spans. + // BufferType.SPANNABLE allows mutable spans, but causes issues with ellipsized texts + // (See b/157754626). + carUiTextView.setText( + CarUiTextUtils.fromCarText(templateContext, text, textParams, carUiTextView.getMaxLines())); + } + + /** @see #mHasInactiveFocus */ + private void setInactiveFocus(boolean hasInactiveFocus) { + if (mHasInactiveFocus != hasInactiveFocus) { + mHasInactiveFocus = hasInactiveFocus; + + // Refresh the drawable state so that it includes the inactive focus state. + refreshDrawableState(); + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridItemWrapper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridItemWrapper.java new file mode 100644 index 0000000..8c40bad --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridItemWrapper.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import androidx.annotation.Nullable; +import androidx.car.app.model.GridItem; +import com.android.car.libraries.apphost.template.view.model.SelectionGroup; + +/** A host side wrapper for {@link GridItem}. */ +public class GridItemWrapper { + private final GridItem mGridItem; + private final int mGridItemIndex; + + /** + * The selection group this grid item belongs to, or {@code null} if the grid item does not belong + * to one. + * + * <p>Selection groups are used to establish mutually-exclusive scopes of grid item selection. + */ + @Nullable private final SelectionGroup mSelectionGroup; + + /** Returns a {@link Builder} that wraps a grid item with the provided index. */ + public static Builder wrap( + GridItem gridItem, int gridItemIndex, @Nullable SelectionGroup selectionGroup) { + Builder builder = new Builder(gridItem, gridItemIndex); + if (selectionGroup != null) { + builder.setSelectionGroup(selectionGroup); + } + return builder; + } + + private GridItemWrapper(Builder builder) { + mGridItem = builder.mGridItem; + mGridItemIndex = builder.mGridItemIndex; + mSelectionGroup = builder.mSelectionGroup; + } + + @Override + public String toString() { + return "[" + mGridItem + ", group: " + mSelectionGroup + "]"; + } + + /** Returns the actual {@link GridItem} object that this instance is wrapping. */ + public GridItem getGridItem() { + return mGridItem; + } + + /** Returns the absolute index of the grid item in the flattened container list. */ + public int getGridItemIndex() { + return mGridItemIndex; + } + + @Nullable + SelectionGroup getSelectionGroup() { + return mSelectionGroup; + } + + /** The builder class for {@link GridItemWrapper}. */ + public static class Builder { + private final GridItem mGridItem; + private final int mGridItemIndex; + @Nullable private SelectionGroup mSelectionGroup; + + private Builder(GridItem gridItem, int gridItemIndex) { + mGridItem = gridItem; + mGridItemIndex = gridItemIndex; + } + + /** + * Sets the selection group this grid item belongs to, or {@code null} if the grid item does not + * belong to one. + */ + public Builder setSelectionGroup(@Nullable SelectionGroup selectionGroup) { + mSelectionGroup = selectionGroup; + return this; + } + + /** Build the {@link GridItemWrapper}. */ + public GridItemWrapper build() { + return new GridItemWrapper(this); + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridRowWrapper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridRowWrapper.java new file mode 100644 index 0000000..48a9851 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridRowWrapper.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import static java.lang.Math.min; + +import androidx.car.app.model.CarText; +import androidx.car.app.model.GridItem; +import java.util.ArrayList; +import java.util.List; + +/** A host side wrapper for a list of {@link GridItem}s that represent a row of the grid. */ +public class GridRowWrapper { + private final List<GridItemWrapper> mGridRowItems; + private final int mGridRowIndex; + private final int mMaxColsPerGridRow; + + private GridRowWrapper( + List<GridItemWrapper> gridRowItems, int gridRowIndex, int maxColsPerGridRow) { + mGridRowItems = gridRowItems; + mGridRowIndex = gridRowIndex; + mMaxColsPerGridRow = maxColsPerGridRow; + } + + public List<GridItemWrapper> getGridRowItems() { + return mGridRowItems; + } + + public int getGridRowIndex() { + return mGridRowIndex; + } + + public int getMaxColsPerGridRow() { + return mMaxColsPerGridRow; + } + + /** + * Creates a list of {@link GridRowWrapper}s from the provided list of {@link GridItemWrapper}s + * based on the {@code numberOfColumns}. + */ + public static List<GridRowWrapper> create( + List<GridItemWrapper> gridItemWrappers, int numberOfColumns) { + List<GridRowWrapper> gridRowWrappers = new ArrayList<>(); + + int itemCount = gridItemWrappers.size(); + int gridRowIndex = 0; + int beginIndex = 0; + while (beginIndex < itemCount) { + gridRowWrappers.add( + new GridRowWrapper( + gridItemWrappers.subList(beginIndex, min(itemCount, beginIndex + numberOfColumns)), + gridRowIndex, + numberOfColumns)); + gridRowIndex++; + beginIndex += numberOfColumns; + } + + return gridRowWrappers; + } + + /** + * Returns {@code true} if any of the {@link GridItem}s consisting this grid row has a title set. + */ + public boolean hasGridItemsWithTitle() { + for (GridItemWrapper gridItemWrapper : mGridRowItems) { + CarText carText = gridItemWrapper.getGridItem().getTitle(); + if (carText != null && !carText.isEmpty()) { + return true; + } + } + + return false; + } + + /** + * Returns {@code true} if any of the {@link GridItem}s consisting this grid row has a secondary + * line of text set. + */ + public boolean hasGridItemsWithText() { + for (GridItemWrapper gridItemWrapper : mGridRowItems) { + CarText carText = gridItemWrapper.getGridItem().getText(); + if (carText != null && !carText.isEmpty()) { + return true; + } + } + + return false; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridView.java new file mode 100644 index 0000000..5a2890a --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridView.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import android.content.Context; +import android.content.res.TypedArray; +import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver; +import android.util.AttributeSet; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import androidx.annotation.Nullable; +import androidx.annotation.StyleableRes; +import androidx.car.app.model.CarText; +import androidx.car.app.model.GridItem; +import androidx.car.app.model.OnItemVisibilityChangedDelegate; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction; +import com.android.car.libraries.apphost.view.common.CarTextUtils; +import com.android.car.libraries.templates.host.R; +import com.android.car.ui.recyclerview.CarUiLayoutStyle; +import com.android.car.ui.recyclerview.CarUiRecyclerView; +import com.android.car.ui.recyclerview.CarUiRecyclerView.CarUiRecyclerViewLayout; +import com.android.car.ui.widget.CarUiTextView; +import java.util.Objects; + +/** A view that can render a grid of {@link GridItem}s wrapped inside a {@link GridWrapper}. */ +public class GridView extends FrameLayout { + private final AdapterDataObserver mAdapterDataObserver = + new AdapterDataObserver() { + // call to update() not allowed on the given receiver. + @SuppressWarnings("nullness:method.invocation") + @Override + public void onChanged() { + super.onChanged(); + update(); + } + }; + + /** The number of items in a grid row. */ + private final int mItemsPerRow; + + private GridAdapter mGridRowAdapter; + + private ViewGroup mProgressContainer; + private CarUiTextView mEmptyListTextView; + private CarUiRecyclerView mRecyclerView; + private RowVisibilityObserver mRowVisibilityObserver; + private boolean mIsLoading; + + public GridView(Context context) { + this(context, null); + } + + public GridView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public GridView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + @SuppressWarnings({"ResourceType", "nullness:method.invocation", "nullness:argument"}) + public GridView( + Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + @StyleableRes + final int[] themeAttrs = { + R.attr.templateGridItemsPerRow, + }; + + TypedArray ta = context.obtainStyledAttributes(themeAttrs); + mItemsPerRow = ta.getInteger(0, 0); + ta.recycle(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mProgressContainer = findViewById(R.id.progress_container); + mEmptyListTextView = findViewById(R.id.list_no_items_text); + mRecyclerView = findViewById(R.id.grid_paged_list_view); + mRecyclerView.setLayoutStyle( + new CarUiLayoutStyle() { + @Override + public int getSpanCount() { + return mItemsPerRow; + } + + @Override + public int getLayoutType() { + return CarUiRecyclerViewLayout.GRID; + } + + @Override + public int getOrientation() { + return CarUiLayoutStyle.VERTICAL; + } + + @Override + public boolean getReverseLayout() { + return false; + } + + @Override + public int getSize() { + return CarUiRecyclerView.SIZE_LARGE; + } + }); + mRowVisibilityObserver = RowVisibilityObserver.create(Objects.requireNonNull(mRecyclerView)); + mGridRowAdapter = GridAdapter.create(getContext(), mItemsPerRow); + mRecyclerView.setAdapter(mGridRowAdapter); + mGridRowAdapter.registerAdapterDataObserver(mAdapterDataObserver); + update(); + } + + void setGrid(TemplateContext templateContext, GridWrapper gridWrapper) { + boolean isLoading = gridWrapper.isLoading(); + if (mIsLoading != isLoading) { + // Trigger a visibility update if the loading state has changed. + mIsLoading = isLoading; + update(); + + if (mIsLoading) { + // Do not update the GridPagedListView/GridRowAdapter, as we want to maintain the + // grid items list size during the loading phase until the new content is populated. + return; + } + } + + CarText emptyListCarText = gridWrapper.getEmptyListText(); + CharSequence emptyText; + if (emptyListCarText != null && !emptyListCarText.isEmpty()) { + emptyText = + CarTextUtils.toCharSequenceOrEmpty(templateContext, gridWrapper.getEmptyListText()); + } else { + emptyText = + templateContext.getText( + templateContext.getHostResourceIds().getTemplateListNoItemsText()); + } + mEmptyListTextView.setText( + CarUiTextUtils.fromCharSequence( + templateContext, emptyText, mEmptyListTextView.getMaxLines())); + mRowVisibilityObserver.setOnItemVisibilityChangedListener( + (startIndexInclusive, endIndexExclusive) -> { + OnItemVisibilityChangedDelegate onItemVisibilityChangedDelegate = + gridWrapper.getOnItemVisibilityChangedDelegate(); + if (onItemVisibilityChangedDelegate != null) { + templateContext + .getAppDispatcher() + .dispatchItemVisibilityChanged( + onItemVisibilityChangedDelegate, startIndexInclusive, endIndexExclusive); + } + }); + + mGridRowAdapter.setGridItems(templateContext, gridWrapper.getGridItemWrappers()); + + if (!gridWrapper.isRefresh()) { + mRecyclerView.scrollToPosition(0); + } + + ViewUtils.logCarAppTelemetry( + templateContext, UiAction.GRID_ITEM_LIST_SIZE, gridWrapper.getGridItemWrappers().size()); + } + + private void update() { + boolean isLoading = mIsLoading; + if (isLoading) { + mProgressContainer.setVisibility(VISIBLE); + + // Mark the content views as invisible so that the size of the container remains the + // same + // while the progress bar is showing. + mEmptyListTextView.setVisibility(INVISIBLE); + mRecyclerView.setVisibility(INVISIBLE); + return; + } + + mProgressContainer.setVisibility(GONE); + + // If the grid item list is empty, hide it and display a message instead. + boolean isEmpty = mGridRowAdapter.getItemCount() == 0; + if (isEmpty) { + mEmptyListTextView.setVisibility(VISIBLE); + mRecyclerView.setVisibility(GONE); + + // When the empty list text view is displayed, show the focus ring by not clipping + // children. + setClipChildren(false); + mEmptyListTextView.setFocusable(true); + } else { + mEmptyListTextView.setVisibility(GONE); + mRecyclerView.setVisibility(VISIBLE); + + // When the grid view is displayed, clip its rows that get out of the view boundary. + setClipChildren(true); + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridWrapper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridWrapper.java new file mode 100644 index 0000000..688e29c --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridWrapper.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import androidx.annotation.Nullable; +import androidx.car.app.model.CarText; +import androidx.car.app.model.GridItem; +import androidx.car.app.model.GridTemplate; +import androidx.car.app.model.Item; +import androidx.car.app.model.ItemList; +import androidx.car.app.model.OnItemVisibilityChangedDelegate; +import androidx.car.app.model.OnSelectedDelegate; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.template.view.model.SelectionGroup; +import com.google.common.collect.ImmutableList; +import java.util.List; + +/** A host side wrapper for {@link ItemList} that's part of a {@link GridTemplate}. */ +public class GridWrapper { + private final boolean mIsLoading; + private final boolean mIsRefresh; + @Nullable private final CarText mEmptyListText; + @Nullable private final OnItemVisibilityChangedDelegate mOnItemVisibilityChangedDelegate; + private final List<GridItemWrapper> mGridItemWrappers; + + /** Converts an {@link ItemList} into a {@link GridWrapper.Builder}. */ + public static Builder wrap(@Nullable ItemList itemList) { + if (itemList == null) { + return new Builder(); + } + + List<Item> gridItems = itemList.getItems(); + Builder builder = + new Builder() + .setGridItems(gridItems) + .setEmptyListText(itemList.getNoItemsMessage()) + .setOnItemVisibilityChangedDelegate(itemList.getOnItemVisibilityChangedDelegate()); + + OnSelectedDelegate onSelectedDelegate = itemList.getOnSelectedDelegate(); + if (onSelectedDelegate != null) { + builder.setSelectionGroup( + SelectionGroup.create( + 0, gridItems.size() - 1, itemList.getSelectedIndex(), onSelectedDelegate)); + } + + return builder; + } + + private GridWrapper(Builder builder) { + mIsLoading = builder.mIsLoading; + mIsRefresh = builder.mIsRefresh; + mEmptyListText = builder.mEmptyListText; + mOnItemVisibilityChangedDelegate = builder.mOnItemVisibilityChangedDelegate; + mGridItemWrappers = buildGridItemWrappers(builder.mGridItems, builder.mSelectionGroup); + } + + @Nullable + public OnItemVisibilityChangedDelegate getOnItemVisibilityChangedDelegate() { + return mOnItemVisibilityChangedDelegate; + } + + public boolean isEmpty() { + return mGridItemWrappers.isEmpty(); + } + + @Nullable + CarText getEmptyListText() { + return mEmptyListText; + } + + List<GridItemWrapper> getGridItemWrappers() { + return mGridItemWrappers; + } + + boolean isLoading() { + return mIsLoading; + } + + boolean isRefresh() { + return mIsRefresh; + } + + /** Builds the {@link GridItemWrapper}s for a given list. */ + private static ImmutableList<GridItemWrapper> buildGridItemWrappers( + @Nullable List<Item> gridItems, @Nullable SelectionGroup selectionGroup) { + if (gridItems == null || gridItems.isEmpty()) { + return ImmutableList.of(); + } + + int beginIndex = 0; + ImmutableList.Builder<GridItemWrapper> gridItemWrapperBuilder = new ImmutableList.Builder<>(); + for (Item item : gridItems) { + if (!(item instanceof GridItem)) { + L.w(LogTags.TEMPLATE, "Item in list is not a GridItem, dropping item"); + } + gridItemWrapperBuilder.add( + GridItemWrapper.wrap((GridItem) item, beginIndex, selectionGroup).build()); + beginIndex++; + } + + return gridItemWrapperBuilder.build(); + } + + /** The builder class for {@link GridWrapper}. */ + public static class Builder { + @Nullable private List<Item> mGridItems; + @Nullable private OnItemVisibilityChangedDelegate mOnItemVisibilityChangedDelegate; + private boolean mIsLoading; + private boolean mIsRefresh; + @Nullable private CarText mEmptyListText; + @Nullable private SelectionGroup mSelectionGroup; + + private Builder() { + mGridItems = null; + } + + /** Sets the grid items in the {@link Builder} */ + public Builder setGridItems(List<Item> gridItems) { + mGridItems = gridItems; + return this; + } + + /** + * Sets the {@link OnItemVisibilityChangedDelegate} which can receive callbacks when the + * visibility of a items changes. + * + * <p>If set to {@code null} it will clear the delegate and no callbacks will be received. + */ + public Builder setOnItemVisibilityChangedDelegate( + @Nullable OnItemVisibilityChangedDelegate delegate) { + mOnItemVisibilityChangedDelegate = delegate; + return this; + } + + /** + * Sets whether the list is loading. + * + * <p>If set to {@code true}, the UI shows a loading indicator and ignore any grid items added + * to the list. If set to {@code false}, the UI shows the actual grid item contents. + */ + public Builder setIsLoading(boolean isLoading) { + mIsLoading = isLoading; + return this; + } + + /** + * Sets whether the grid is a refresh of the existing grid. + * + * <p>If set to {@code true}, the UI will not scroll to top, otherwise it will. + */ + public Builder setIsRefresh(boolean isRefresh) { + mIsRefresh = isRefresh; + return this; + } + + /** Sets the text to be displayed when there are no items in the list. */ + public Builder setEmptyListText(@Nullable CarText emptyListText) { + mEmptyListText = emptyListText; + return this; + } + + /** + * Sets the selection group these grid items belong to, or {@code null} if the grid items do not + * belong to one. + */ + public Builder setSelectionGroup(@Nullable SelectionGroup selectionGroup) { + mSelectionGroup = selectionGroup; + return this; + } + + /** Builds the {@link GridWrapper}. */ + public GridWrapper build() { + return new GridWrapper(this); + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/HeaderView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/HeaderView.java new file mode 100644 index 0000000..56ec415 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/HeaderView.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import android.view.View; +import androidx.annotation.Nullable; +import androidx.car.app.model.Action; +import androidx.car.app.model.CarText; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.view.common.CarTextUtils; +import com.android.car.ui.core.CarUi; +import com.android.car.ui.toolbar.ToolbarController; + +/** A view that displays the header for the templates. */ +public class HeaderView extends AbstractHeaderView { + + private HeaderView(TemplateContext templateContext, ToolbarController toolbarController) { + super(templateContext, toolbarController); + } + + /** + * Set or clear the content of the view. + * + * <p>If the {@code title} is {@code null} then the view is hidden. + */ + public void setContent( + TemplateContext templateContext, @Nullable CarText title, @Nullable Action action) { + mToolbarController.setTitle(CarTextUtils.toCharSequenceOrEmpty(templateContext, title)); + setAction(action); + } + + /** Installs a {@link HeaderView} around the given container view. */ + @SuppressWarnings("nullness:argument") // InsetsChangedListener is nullable. + public static HeaderView install(TemplateContext mTemplateContext, View container) { + ToolbarController toolbarController = CarUi.installBaseLayoutAround(container, null, true); + if (toolbarController == null) { + throw new NullPointerException("Toolbar Controller could not be created."); + } + return new HeaderView(mTemplateContext, toolbarController); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/InputSignInView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/InputSignInView.java new file mode 100644 index 0000000..c8932d2 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/InputSignInView.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import static android.text.InputType.TYPE_CLASS_NUMBER; +import static android.text.InputType.TYPE_CLASS_PHONE; +import static android.text.InputType.TYPE_CLASS_TEXT; +import static android.text.InputType.TYPE_NUMBER_VARIATION_PASSWORD; +import static android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS; +import static android.text.InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; +import static android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD; +import static androidx.car.app.model.signin.InputSignInMethod.INPUT_TYPE_PASSWORD; +import static androidx.car.app.model.signin.InputSignInMethod.KEYBOARD_DEFAULT; +import static androidx.car.app.model.signin.InputSignInMethod.KEYBOARD_EMAIL; +import static androidx.car.app.model.signin.InputSignInMethod.KEYBOARD_NUMBER; +import static androidx.car.app.model.signin.InputSignInMethod.KEYBOARD_PHONE; + +import android.content.Context; +import android.content.res.TypedArray; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.widget.LinearLayout; +import androidx.annotation.Nullable; +import androidx.annotation.StyleableRes; +import androidx.car.app.model.CarText; +import androidx.car.app.model.InputCallbackDelegate; +import androidx.car.app.model.signin.InputSignInMethod; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.input.CarEditable; +import com.android.car.libraries.apphost.input.CarEditableListener; +import com.android.car.libraries.apphost.input.InputManager; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.view.common.CarTextUtils; +import com.android.car.libraries.templates.host.R; +import com.android.car.ui.widget.CarUiTextView; + +/** A view that displays {@link InputSignInMethod} UI. */ +public class InputSignInView extends LinearLayout implements CarEditable { + private final int mMaxWidth; + + private CarEditText mSignInEditText; + private CarUiTextView mSignInEditTextErrorMessage; + @Nullable private TextWatcher mTextWatcher; + + public InputSignInView(Context context) { + this(context, null); + } + + public InputSignInView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public InputSignInView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + @SuppressWarnings("ResourceType") + public InputSignInView( + Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + @StyleableRes + final int[] themeAttrs = { + R.attr.templateSignInMethodViewMaxWidth, + }; + + TypedArray ta = context.obtainStyledAttributes(themeAttrs); + mMaxWidth = ta.getDimensionPixelSize(0, 0); + ta.recycle(); + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo editorInfo) { + return mSignInEditText.onCreateInputConnection(editorInfo); + } + + @Override + public void setCarEditableListener(CarEditableListener listener) {} + + @Override + public void setInputEnabled(boolean enabled) {} + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); + if (mMaxWidth > 0 && mMaxWidth < measuredWidth) { + int measureMode = MeasureSpec.getMode(widthMeasureSpec); + widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, measureMode); + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + /** Sets the {@link InputSignInMethod} for the view. */ + public void setSignInMethod( + TemplateContext templateContext, + InputSignInMethod inputSignInMethod, + InputManager inputManager, + CharSequence disabledInputHint, + boolean isRefresh) { + clearEditTextListeners(); + + // TODO(b/183434044): move this logic to CarRestrictedEditText + setInputHint(templateContext, inputSignInMethod, disabledInputHint); + + // TODO(b/183434044): move this logic to CarRestrictedEditText + setInitialText(templateContext, inputSignInMethod, isRefresh); + + setErrorMessage(templateContext, inputSignInMethod); + + // TODO(b/183434044): move this logic to CarRestrictedEditText + setInputType(inputSignInMethod); + + setShowKeyboardByDefault(templateContext, inputSignInMethod, inputManager); + + // Make sure to set these at the end so that setting initial text etc doesn't trigger text + // change callbacks. + setEditTextListeners(templateContext, inputSignInMethod, inputManager); + } + + /** Clears the edit text focus. */ + public void clearEditTextFocus() { + mSignInEditText.clearFocus(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mSignInEditText = findViewById(R.id.input_sign_in_box); + mSignInEditTextErrorMessage = findViewById(R.id.input_sign_in_error_message); + } + + private void clearEditTextListeners() { + mSignInEditText.setOnClickListener(null); + mSignInEditText.setOnEditorActionListener(null); + if (mTextWatcher != null) { + mSignInEditText.removeTextChangedListener(mTextWatcher); + mTextWatcher = null; + } + } + + private void setEditTextListeners( + TemplateContext templateContext, + InputSignInMethod inputSignInMethod, + InputManager inputManager) { + mSignInEditText.setOnEditorActionListener( + (view, actionId, event) -> { + inputManager.stopInput(); + String inputText = mSignInEditText.getText().toString().trim(); + if (TextUtils.isEmpty(inputText)) { + return false; + } else { + submitInput(templateContext, inputText, inputSignInMethod); + return true; + } + }); + mTextWatcher = + new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence text, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence text, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable text) { + updateInputText(templateContext, text.toString(), inputSignInMethod); + } + }; + mSignInEditText.addTextChangedListener(mTextWatcher); + mSignInEditText.setInputManager(inputManager); + } + + private void setInputHint( + TemplateContext templateContext, + InputSignInMethod inputSignInMethod, + CharSequence disabledInputHint) { + CarText inputHint = inputSignInMethod.getHint(); + CharSequence hint = CarTextUtils.toCharSequenceOrEmpty(templateContext, inputHint); + mSignInEditText.setHint(mSignInEditText.isEnabled() ? hint : disabledInputHint); + } + + private void setInitialText( + TemplateContext templateContext, InputSignInMethod inputSignInMethod, boolean isRefresh) { + CarText initialText = inputSignInMethod.getDefaultValue(); + + CharSequence text = mSignInEditText.getText(); + if (!isRefresh || text == null || text.length() == 0) { + mSignInEditText.setText(CarTextUtils.toCharSequenceOrEmpty(templateContext, initialText)); + mSignInEditText.setSelection(mSignInEditText.getText().length()); + } + } + + private void setErrorMessage( + TemplateContext templateContext, InputSignInMethod inputSignInMethod) { + CarText errorMessage = inputSignInMethod.getErrorMessage(); + if (!CarText.isNullOrEmpty(errorMessage)) { + mSignInEditTextErrorMessage.setText( + CarUiTextUtils.fromCarText( + templateContext, errorMessage, mSignInEditTextErrorMessage.getMaxLines())); + mSignInEditTextErrorMessage.setVisibility(VISIBLE); + mSignInEditText.setErrorState(true); + } else { + mSignInEditTextErrorMessage.setVisibility(GONE); + mSignInEditText.setErrorState(false); + } + } + + private void setInputType(InputSignInMethod inputSignInMethod) { + int inputType; + switch (inputSignInMethod.getKeyboardType()) { + case KEYBOARD_PHONE: + inputType = TYPE_CLASS_PHONE; + break; + case KEYBOARD_NUMBER: + inputType = TYPE_CLASS_NUMBER; + if (inputSignInMethod.getInputType() == INPUT_TYPE_PASSWORD) { + inputType |= TYPE_NUMBER_VARIATION_PASSWORD; + } + break; + case KEYBOARD_EMAIL: + inputType = + TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_EMAIL_ADDRESS | TYPE_TEXT_FLAG_NO_SUGGESTIONS; + break; + case KEYBOARD_DEFAULT: + default: + inputType = TYPE_CLASS_TEXT | TYPE_TEXT_FLAG_NO_SUGGESTIONS; + if (inputSignInMethod.getInputType() == INPUT_TYPE_PASSWORD) { + inputType |= TYPE_TEXT_VARIATION_PASSWORD; + } + } + mSignInEditText.setInputType(inputType); + } + + private void setShowKeyboardByDefault( + TemplateContext templateContext, + InputSignInMethod inputSignInMethod, + InputManager inputManager) { + boolean isRestricted = templateContext.getConstraintsProvider().isConfigRestricted(); + if (inputSignInMethod.isShowKeyboardByDefault() && !isRestricted) { + inputManager.startInput(this); + } + } + + private void submitInput( + TemplateContext templateContext, String inputText, InputSignInMethod inputSignInMethod) { + InputCallbackDelegate delegate = inputSignInMethod.getInputCallbackDelegate(); + if (delegate != null) { + templateContext.getAppDispatcher().dispatchInputSubmitted(delegate, inputText); + } else { + L.w(LogTags.TEMPLATE, "Input callback is expected on the template but not set"); + } + } + + private void updateInputText( + TemplateContext templateContext, String inputText, InputSignInMethod inputSignInMethod) { + InputCallbackDelegate delegate = inputSignInMethod.getInputCallbackDelegate(); + if (delegate != null) { + templateContext.getAppDispatcher().dispatchInputTextChanged(delegate, inputText); + } else { + L.w(LogTags.TEMPLATE, "Input callback is expected on the template but not set"); + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/MarkerFactory.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/MarkerFactory.java new file mode 100644 index 0000000..fa1db8c --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/MarkerFactory.java @@ -0,0 +1,592 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import static com.android.car.libraries.apphost.view.common.ImageUtils.SCALE_CENTER_XY_INSIDE; +import static com.android.car.libraries.apphost.view.common.ImageUtils.SCALE_FIT_CENTER; +import static java.lang.Math.max; +import static java.util.Objects.requireNonNull; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Paint.Cap; +import android.graphics.Path; +import android.graphics.PorterDuff; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; +import androidx.car.app.model.CarColor; +import androidx.car.app.model.CarIcon; +import androidx.car.app.model.CarText; +import androidx.car.app.model.PlaceMarker; +import com.android.car.libraries.apphost.common.CarColorUtils; +import com.android.car.libraries.apphost.common.CommonUtils; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.distraction.constraints.CarColorConstraints; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.view.common.ImageUtils; +import com.android.car.libraries.apphost.view.common.ImageViewParams; +import com.android.car.libraries.templates.host.R; +import org.checkerframework.checker.initialization.qual.UnderInitialization; +import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** A factory of bitmaps to be used as map markers in different templates. */ +public class MarkerFactory { + // Cache the default anchor so that we don't have to draw if users did not customize. + private final Bitmap mDefaultAnchorBitmap; + + // Cache the default marker so that we don't have to draw if users did not customize. + private final Bitmap mDefaultMarkerBitmap; + + // Mask for clipping an image within bounds. A marker image's draw area is slightly bigger than + // icons with rounded corners. + @MonotonicNonNull private Bitmap mMarkerImageMask; + private final Paint mMarkerImageMaskPaint; + + // Default path used for drawing the standard-size map marker. + private final Path mDefaultMarkerPath; + private final MarkerAppearance mAppearance; + + /** Create a MarkerFactory */ + public static MarkerFactory create(Context context, MarkerAppearance appearance) { + return new MarkerFactory(context, appearance); + } + + /** Returns a map marker bitmap with the given {@link PlaceMarker} configuration. */ + // TODO(b/144920236): cache and reuse bitmaps when applicable. + public Bitmap createPoiMarkerBitmap( + TemplateContext templateContext, @Nullable PlaceMarker marker) { + if (marker == null) { + return mDefaultMarkerBitmap; + } + + // Use the dark variant for background color. + @ColorInt int markerColor = resolveMarkerColor(templateContext, marker); + boolean useDefaultMarker = markerColor == mAppearance.mMarkerDefaultBackgroundColor; + + CarText label = marker.getLabel(); + CarIcon icon = marker.getIcon(); + if (label == null && icon == null && useDefaultMarker) { + return mDefaultMarkerBitmap; + } + + boolean needWideMarker = false; + Bitmap markerBitmap; + String labelString = label == null ? null : label.toString(); + if (icon == null && labelString != null) { + // If we need to draw a label, check if we need to draw a wider marker to fit the text. + Rect bounds = new Rect(); + mAppearance.mDefaultTextPaint.getTextBounds(labelString, 0, labelString.length(), bounds); + + if (bounds.width() > mAppearance.mMarkerSize - mAppearance.mTextHorizontalPadding * 2) { + needWideMarker = true; + markerBitmap = + Bitmap.createBitmap( + bounds.width() + mAppearance.mTextHorizontalPadding * 2, + mAppearance.mMarkerSize + mAppearance.mMarkerPointerHeight, + Config.ARGB_8888); + markerBitmap.setDensity(mAppearance.mDensityDpi); + } else { + markerBitmap = Bitmap.createBitmap(mDefaultMarkerBitmap); + } + } else { + markerBitmap = Bitmap.createBitmap(mDefaultMarkerBitmap); + } + + Canvas canvas = new Canvas(markerBitmap); + if (needWideMarker || !useDefaultMarker) { + drawMarker( + canvas, + mAppearance, + markerColor, + useDefaultMarker + ? mAppearance.mMarkerDefaultBorderColor + : mAppearance.mMarkerCustomBorderColor); + } + + Bitmap contentBitmap = getContentForMapMarker(templateContext, marker, useDefaultMarker); + if (contentBitmap != null) { + canvas.drawBitmap( + contentBitmap, + // Width may have been adjusted so we use the bitmap's width as source of truth. + (markerBitmap.getWidth() - contentBitmap.getWidth()) / 2f, + (mAppearance.mMarkerSize - contentBitmap.getHeight()) / 2f, + mAppearance.mDefaultTextPaint); + } + + return markerBitmap; + } + + /** + * Returns the bitmap representing the content (icon of text) that should appear within a marker + * in the map view, or {code null} if the marker's content is not specified. + */ + @Nullable + private Bitmap getContentForMapMarker( + TemplateContext templateContext, PlaceMarker marker, boolean hasDefaultBackground) { + CarText label = marker.getLabel(); + String labelString = label != null ? label.toString() : null; + CarIcon icon = marker.getIcon(); + Bitmap contentBitmap = null; + + // The icon value takes precedence over the label if both are set. + if (icon != null) { + contentBitmap = + getIconBitmap(templateContext, marker, icon, CommonUtils.isDarkMode(templateContext)); + } else if (labelString != null && !labelString.isEmpty()) { + contentBitmap = + ImageUtils.getBitmapFromString( + labelString, + hasDefaultBackground + ? mAppearance.mDefaultTextPaint + : mAppearance.mCustomBackgroundTextPaint); + } + + if (contentBitmap != null) { + contentBitmap.setDensity(mAppearance.mDensityDpi); + } + + return contentBitmap; + } + + /** + * Returns the bitmap representing the content (icon of text) of the given marker, or {code null} + * if the marker's content is not specified. + */ + @Nullable + public Bitmap getContentForListMarker(TemplateContext templateContext, PlaceMarker marker) { + CarText label = marker.getLabel(); + String labelString = label != null ? label.toString() : null; + CarIcon icon = marker.getIcon(); + Bitmap contentBitmap = null; + + // The icon value takes precedence over the label if both are set. + if (icon != null) { + // We always use the light-variant tint for list marker because the card background is + // dark. + contentBitmap = getIconBitmap(templateContext, marker, icon, /* isDark= */ false); + } else if (labelString != null && !labelString.isEmpty()) { + // We use the light-variant color for the text in the list. + int resolvedColor = + CarColorUtils.resolveColor( + templateContext, + marker.getColor(), + + // The background of the card is dark so use the light variant for the + // text color. + /* isDark= */ false, + mAppearance.mDefaultTextPaint.getColor(), + CarColorConstraints.UNCONSTRAINED); + Paint paint; + if (resolvedColor == mAppearance.mCustomBackgroundTextPaint.getColor()) { + paint = mAppearance.mCustomBackgroundTextPaint; + } else { + paint = new Paint(mAppearance.mCustomBackgroundTextPaint); + paint.setColor(resolvedColor); + } + + Resources resources = templateContext.getResources(); + Drawable bitmap = + new BitmapDrawable(resources, ImageUtils.getBitmapFromString(labelString, paint)); + contentBitmap = + ImageUtils.getBitmapFromDrawable( + bitmap, + mAppearance.mListIconSize, + mAppearance.mListIconSize, + resources.getDisplayMetrics().densityDpi, + SCALE_CENTER_XY_INSIDE); + } + + if (contentBitmap != null) { + contentBitmap.setDensity(mAppearance.mDensityDpi); + } + + return contentBitmap; + } + + @Nullable + private Bitmap getIconBitmap( + TemplateContext templateContext, PlaceMarker marker, CarIcon icon, boolean isDark) { + Bitmap contentBitmap; + boolean isImage = isMarkerImage(marker); + int bitmapSize = isImage ? mAppearance.mImageSize : mAppearance.mIconSize; + ImageViewParams imageParams = + ImageViewParams.builder() + .setDefaultTint(mAppearance.mDefaultIconTint) + .setForceTinting(!isImage) + .setIsDark(isDark) + .build(); + contentBitmap = + ImageUtils.getBitmapFromIcon( + templateContext, icon, bitmapSize, bitmapSize, imageParams, SCALE_FIT_CENTER); + + if (contentBitmap == null) { + L.e(LogTags.TEMPLATE, "Failed to get bitmap for marker: %s", marker); + } else if (isImage) { + // Apply masking to get the rounded corner effect. + Bitmap maskedImage = Bitmap.createBitmap(bitmapSize, bitmapSize, Config.ARGB_8888); + maskedImage.setDensity(mAppearance.mDensityDpi); + + Canvas maskedCanvas = new Canvas(maskedImage); + maskedCanvas.drawBitmap(getOrCreateMarkerImageMask(mAppearance), 0, 0, null); + maskedCanvas.drawBitmap(contentBitmap, 0, 0, mMarkerImageMaskPaint); + contentBitmap = maskedImage; + } + return contentBitmap; + } + + /** Returns an {@link Bitmap} which has been adjusted for a given background color. */ + public Bitmap getAnchorBitmap(TemplateContext templateContext, @Nullable CarColor background) { + if (background == null) { + return mDefaultAnchorBitmap; + } + + @ColorInt + int resolvedBackground = + CarColorUtils.resolveColor( + templateContext, + background, + // Use the dark-variant in day mode, and vice versa. + /* isDark= */ !CommonUtils.isDarkMode(templateContext), + mAppearance.mAnchorDefaultBackgroundColor, + CarColorConstraints.UNCONSTRAINED); + if (resolvedBackground == mAppearance.mAnchorDefaultBackgroundColor) { + return mDefaultAnchorBitmap; + } + + return createAnchorBitmap(templateContext, resolvedBackground, mAppearance); + } + + /** Draw a rounded-corner marker with a pointer at the bottom center. */ + private void drawMarker( + @UnknownInitialization MarkerFactory this, + Canvas canvas, + MarkerAppearance appearance, + @ColorInt int backgroundColor, + @ColorInt int borderColor) { + int defaultMarkerWidth = appearance.mMarkerSize; + int defaultMarkerHeight = appearance.mMarkerSize + appearance.mMarkerPointerHeight; + + Path markerPath = + canvas.getWidth() == defaultMarkerWidth + ? mDefaultMarkerPath + : createMarkerPath(canvas.getWidth(), defaultMarkerHeight, appearance); + if (markerPath == null) { + return; + } + + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setStyle(Paint.Style.FILL); + paint.setColor(backgroundColor); + canvas.drawPath(markerPath, paint); + + paint.setStyle(Paint.Style.STROKE); + paint.setColor(borderColor); + paint.setStrokeWidth(appearance.mMarkerStroke); + paint.setStrokeCap(Cap.ROUND); + canvas.drawPath(markerPath, paint); + } + + private Bitmap createDefaultMarkerBitmap( + @UnderInitialization MarkerFactory this, MarkerAppearance appearance) { + Bitmap markerBitmap = + Bitmap.createBitmap( + appearance.mMarkerSize, + appearance.mMarkerSize + appearance.mMarkerPointerHeight, + Config.ARGB_8888); + markerBitmap.setDensity(appearance.mDensityDpi); + + Canvas canvas = new Canvas(markerBitmap); + drawMarker( + canvas, + appearance, + appearance.mMarkerDefaultBackgroundColor, + appearance.mMarkerDefaultBorderColor); + + return markerBitmap; + } + + private Bitmap createAnchorBitmap( + @UnknownInitialization MarkerFactory this, + Context context, + @ColorInt int backgroundColor, + MarkerAppearance appearance) { + Resources resources = context.getResources(); + + Drawable markerBackground = resources.getDrawable(R.drawable.anchor_marker); + markerBackground.setBounds( + 0, 0, markerBackground.getIntrinsicWidth(), markerBackground.getIntrinsicHeight()); + markerBackground.setColorFilter(backgroundColor, PorterDuff.Mode.SRC_IN); + + Drawable markerBorder = resources.getDrawable(R.drawable.anchor_marker_border); + markerBorder.setBounds( + 0, 0, markerBorder.getIntrinsicWidth(), markerBorder.getIntrinsicHeight()); + markerBorder.setColorFilter(appearance.mAnchorBorderColor, PorterDuff.Mode.SRC_IN); + + Drawable markerDot = resources.getDrawable(R.drawable.anchor_marker_circle); + markerDot.setBounds(0, 0, markerDot.getIntrinsicWidth(), markerDot.getIntrinsicHeight()); + markerDot.setColorFilter(appearance.mAnchorDotColor, PorterDuff.Mode.SRC_IN); + + Bitmap bitmap = + Bitmap.createBitmap( + markerBackground.getIntrinsicWidth(), + markerBackground.getIntrinsicHeight(), + Config.ARGB_8888); + bitmap.setDensity(appearance.mDensityDpi); + + Canvas canvas = new Canvas(bitmap); + markerBackground.draw(canvas); + markerBorder.draw(canvas); + markerDot.draw(canvas); + + return bitmap; + } + + private Bitmap getOrCreateMarkerImageMask(MarkerAppearance appearance) { + if (mMarkerImageMask != null) { + return mMarkerImageMask; + } + + mMarkerImageMask = + Bitmap.createBitmap(appearance.mImageSize, appearance.mImageSize, Config.ALPHA_8); + mMarkerImageMask.setDensity(appearance.mDensityDpi); + + Canvas canvas = new Canvas(mMarkerImageMask); + canvas.drawRoundRect( + 0, + 0, + appearance.mImageSize, + appearance.mImageSize, + appearance.mImageCornerRadius, + appearance.mImageCornerRadius, + new Paint()); + + return mMarkerImageMask; + } + + /** + * Returns the marker color that should be used based on the {@link PlaceMarker}'s configuration. + * + * <p>If the marker is of type {@link PlaceMarker#TYPE_IMAGE}, then the default color will be + * used. Otherwise, we resolve the color provided via the marker to what is defined in our theme. + */ + @ColorInt + private int resolveMarkerColor(TemplateContext templateContext, PlaceMarker marker) { + // We do not support rendering a background color for images. + if (marker.getIconType() == PlaceMarker.TYPE_IMAGE) { + return mAppearance.mMarkerDefaultBackgroundColor; + } + + @ColorInt + int resolvedColor = + CarColorUtils.resolveColor( + templateContext, + marker.getColor(), + // Use the dark-variant in day mode for better contrast with the + // light-colored map, and + // vice versa. + /* isDark= */ !CommonUtils.isDarkMode(templateContext), + mAppearance.mMarkerDefaultBackgroundColor, + CarColorConstraints.UNCONSTRAINED); + return resolvedColor; + } + + private MarkerFactory(Context context, MarkerAppearance appearance) { + mAppearance = appearance; + + mDefaultMarkerPath = + createMarkerPath( + appearance.mMarkerSize, + appearance.mMarkerSize + appearance.mMarkerPointerHeight, + appearance); + mDefaultMarkerBitmap = createDefaultMarkerBitmap(appearance); + mDefaultAnchorBitmap = + createAnchorBitmap(context, appearance.mAnchorDefaultBackgroundColor, appearance); + mDefaultAnchorBitmap.setDensity(appearance.mDensityDpi); + + mMarkerImageMaskPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mMarkerImageMaskPaint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN)); + } + + /** + * Create a {@link Path} representing a map marker that fits within the input width and height. + */ + private static Path createMarkerPath(int width, int height, MarkerAppearance appearance) { + // Actual draw region needs to account for the stroke size. + // At the end, we offset the drawing by (markerStroke / 2) in both x and y to center it. + int drawWidth = width - appearance.mMarkerStroke; + int drawHeight = height - appearance.mMarkerStroke; + int cornerRadius = appearance.mMarkerCornerRadius; + int cornerDiameter = cornerRadius * 2; + float halfStroke = appearance.mMarkerStroke / 2f; + + // Start from the pointer tip and draw clockwise. + float startX = drawWidth / 2f; + float startY = drawHeight; + + // Bottom of the rectangular region of the marker. + float rectBottom = startY - appearance.mMarkerPointerHeight; + float pointerHalfWidth = appearance.mMarkerPointerWidth / 2f; + + Path path = new Path(); + RectF cornerRect = new RectF(); + + path.moveTo(startX, startY); + path.lineTo(startX - pointerHalfWidth, rectBottom); + path.lineTo(appearance.mMarkerCornerRadius, rectBottom); + cornerRect.set(0, rectBottom - cornerDiameter, cornerDiameter, rectBottom); + path.arcTo(cornerRect, 90, 90, false); + + path.lineTo(0, cornerRadius); + cornerRect = new RectF(0, 0, cornerDiameter, cornerDiameter); + path.arcTo(cornerRect, 180, 90, false); + + path.lineTo(drawWidth - cornerRadius, 0); + cornerRect.set(drawWidth - cornerDiameter, 0, drawWidth, cornerDiameter); + path.arcTo(cornerRect, 270, 90, false); + + path.lineTo(drawWidth, rectBottom - cornerRadius); + cornerRect.set(drawWidth - cornerDiameter, rectBottom - cornerDiameter, drawWidth, rectBottom); + path.arcTo(cornerRect, 0, 90, false); + + path.lineTo(startX + pointerHalfWidth, rectBottom); + path.close(); + + // Offset the path to accommodate the stroke so the drawing is centered within the region. + path.offset(halfStroke, halfStroke); + + return path; + } + + private static boolean isMarkerImage(PlaceMarker marker) { + return marker.getIconType() == PlaceMarker.TYPE_IMAGE; + } + + /** Contains the attributes that define the marker's appearance. */ + public static class MarkerAppearance { + @ColorInt private final int mMarkerDefaultBackgroundColor; + @ColorInt private final int mMarkerDefaultBorderColor; + @ColorInt private final int mMarkerCustomBorderColor; + private final int mMarkerSize; + private final int mMarkerPointerWidth; + private final int mMarkerPointerHeight; + private final int mMarkerStroke; + private final int mMarkerCornerRadius; + @ColorInt private final int mAnchorDefaultBackgroundColor; + @ColorInt private final int mAnchorBorderColor; + @ColorInt private final int mAnchorDotColor; + + @ColorInt private final int mDefaultIconTint; + + private final int mIconSize; + private final int mTextHorizontalPadding; + private final int mImageSize; + private final int mImageCornerRadius; + private final Paint mDefaultTextPaint; + private final Paint mCustomBackgroundTextPaint; + + private final int mDensityDpi; + private final int mListIconSize; + + /** + * Creates an instance of a {@link MarkerAppearance} by reading it from the styled attributes in + * the given context's theme. + */ + public MarkerAppearance( + Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + // Get the marker appearance style resource id from the view's attributes. + TypedArray viewStyledAttributes = + context.obtainStyledAttributes(attrs, R.styleable.PlaceMarker, defStyleAttr, defStyleRes); + int resId = viewStyledAttributes.getResourceId(R.styleable.PlaceMarker_markerAppearance, -1); + viewStyledAttributes.recycle(); + + // No need to pass default values here, the style should contain all these values, which + // can be ensured by using a default style resource by the caller. + TypedArray ta = context.obtainStyledAttributes(resId, R.styleable.MarkerAppearance); + + @ColorInt + int defaultContentColor = + ta.getColor(R.styleable.MarkerAppearance_markerDefaultContentColor, -1); + + // Set up the paint for the text of the marker's label. + mDefaultTextPaint = + new Paint(Paint.LINEAR_TEXT_FLAG | Paint.SUBPIXEL_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG); + mDefaultTextPaint.setTextAlign(Align.CENTER); + mDefaultTextPaint.setTypeface( + Typeface.create( + requireNonNull(ta.getString(R.styleable.MarkerAppearance_android_fontFamily)), + ta.getInt(R.styleable.MarkerAppearance_android_textStyle, -1))); + mDefaultTextPaint.setColor(defaultContentColor); + mDefaultTextPaint.setTextSize( + ta.getDimensionPixelSize(R.styleable.MarkerAppearance_android_textSize, -1)); + + mCustomBackgroundTextPaint = new Paint(mDefaultTextPaint); + mCustomBackgroundTextPaint.setColor( + ta.getColor(R.styleable.MarkerAppearance_markerCustomBackgroundContentColor, -1)); + + // All other marker/anchor related dimensions and colors + mMarkerDefaultBackgroundColor = + ta.getInt(R.styleable.MarkerAppearance_markerDefaultBackgroundColor, -1); + mMarkerDefaultBorderColor = + ta.getInt(R.styleable.MarkerAppearance_markerDefaultBorderColor, -1); + mMarkerCustomBorderColor = + ta.getInt(R.styleable.MarkerAppearance_markerCustomBorderColor, -1); + mMarkerPointerWidth = + ta.getDimensionPixelSize(R.styleable.MarkerAppearance_markerPointerWidth, -1); + mMarkerPointerHeight = + ta.getDimensionPixelSize(R.styleable.MarkerAppearance_markerPointerHeight, -1); + mMarkerStroke = ta.getDimensionPixelSize(R.styleable.MarkerAppearance_markerStroke, -1); + mMarkerCornerRadius = + ta.getDimensionPixelSize(R.styleable.MarkerAppearance_markerCornerRadius, -1); + mAnchorDefaultBackgroundColor = + ta.getInt(R.styleable.MarkerAppearance_anchorDefaultBackgroundColor, -1); + mAnchorBorderColor = ta.getInt(R.styleable.MarkerAppearance_anchorBorderColor, -1); + mAnchorDotColor = ta.getInt(R.styleable.MarkerAppearance_anchorDotColor, -1); + mTextHorizontalPadding = + ta.getDimensionPixelSize(R.styleable.MarkerAppearance_markerTextHorizontalPadding, -1); + mIconSize = ta.getDimensionPixelSize(R.styleable.MarkerAppearance_markerIconSize, -1); + mImageSize = ta.getDimensionPixelSize(R.styleable.MarkerAppearance_markerImageSize, -1); + mImageCornerRadius = + ta.getDimensionPixelSize(R.styleable.MarkerAppearance_markerImageCornerRadius, -1); + + mDefaultIconTint = defaultContentColor; + + int markerPadding = ta.getDimensionPixelSize(R.styleable.MarkerAppearance_markerPadding, -1); + mMarkerSize = max(mIconSize, mImageSize) + mMarkerStroke + markerPadding; + + mListIconSize = ta.getDimensionPixelSize(R.styleable.MarkerAppearance_markerListIconSize, -1); + + ta.recycle(); + + mDensityDpi = context.getResources().getDisplayMetrics().densityDpi; + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/PanOverlayView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/PanOverlayView.java new file mode 100644 index 0000000..607cd18 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/PanOverlayView.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** Overlay view to display on top of the map surface in the pan mode. */ +public class PanOverlayView extends FrameLayout { + + public PanOverlayView(@NonNull Context context) { + this(context, null); + } + + public PanOverlayView(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public PanOverlayView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public PanOverlayView( + @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ParkedOnlyFrameLayout.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ParkedOnlyFrameLayout.java new file mode 100644 index 0000000..34d84e9 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ParkedOnlyFrameLayout.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.android.car.libraries.apphost.common.EventManager; +import com.android.car.libraries.apphost.common.EventManager.EventType; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.templates.host.R; +import com.android.car.ui.widget.CarUiTextView; + +/** + * Custom view that hides content and shows an appropriate message when car is driving. This view + * will hide all children views while the driving message is being shown. Usage of this layout + * should use a single container layout as a child, and visibility of that child should not be + * modified outside of this layout. This layout does not maintain any visibility states of children + * views before or after drive state changes. This means that if the visibility of children views + * are updated directly the visibility may not be consistent after the driving message disappears. + */ +public class ParkedOnlyFrameLayout extends FrameLayout { + + private View mDrivingMessageView; + + private boolean mIsLockedOut; + private TemplateContext mTemplateContext; + private EventManager mEventManager; + + public ParkedOnlyFrameLayout(@NonNull Context context) { + super(context); + } + + public ParkedOnlyFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public ParkedOnlyFrameLayout( + @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public ParkedOnlyFrameLayout( + @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + View rootView = LayoutInflater.from(getContext()).inflate(R.layout.driving_message_view, this); + mDrivingMessageView = rootView.findViewById(R.id.driving_message_view); + } + + @Override + protected void onDetachedFromWindow() { + if (mEventManager != null) { + mEventManager.unsubscribeEvent(this, EventType.CONSTRAINTS); + } + super.onDetachedFromWindow(); + } + + /** Set the template context used to start listening for uxr constraints. */ + public void setTemplateContext(TemplateContext templateContext) { + mTemplateContext = templateContext; + mEventManager = templateContext.getEventManager(); + + mEventManager.subscribeEvent(this, EventType.CONSTRAINTS, this::update); + + CarUiTextView drivingMessageText = mDrivingMessageView.findViewById(R.id.driving_message_text); + drivingMessageText.setText( + CarUiTextUtils.fromCharSequence( + templateContext, + templateContext.getString( + templateContext.getHostResourceIds().getDrivingStateMessageText()), + drivingMessageText.getMaxLines())); + update(); + } + + /** Get whether the content view is being hidden, and the driving message is being shown. */ + public boolean isLockedOut() { + return mIsLockedOut; + } + + private void update() { + boolean isRestricted = mTemplateContext.getConstraintsProvider().isConfigRestricted(); + if (isRestricted == mIsLockedOut) { + return; + } + mIsLockedOut = isRestricted; + + // Hide IME if ParkedOnlyFrameLayout is visible + if (mIsLockedOut) { + mTemplateContext.getInputManager().stopInput(); + } + + // Toggle visibility of all children views; the driving message will be shown if locked out, + // content views are shown otherwise. + for (int i = 0; i < this.getChildCount(); i++) { + View view = getChildAt(i); + if (view.getId() == mDrivingMessageView.getId()) { + view.setVisibility(mIsLockedOut ? VISIBLE : GONE); + } else { + view.setVisibility(mIsLockedOut ? GONE : VISIBLE); + } + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/PinSignInView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/PinSignInView.java new file mode 100644 index 0000000..1282365 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/PinSignInView.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.widget.FrameLayout; +import androidx.annotation.Nullable; +import androidx.annotation.StyleableRes; +import androidx.car.app.model.signin.PinSignInMethod; +import com.android.car.libraries.templates.host.R; +import com.android.car.ui.CarUiText; +import com.android.car.ui.widget.CarUiTextView; + +/** A view that displays {@link PinSignInMethod} UI. */ +public class PinSignInView extends FrameLayout { + private final int mMaxWidth; + + private CarUiTextView mPinTextView; + + public PinSignInView(Context context) { + this(context, null); + } + + public PinSignInView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public PinSignInView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + @SuppressWarnings("ResourceType") + public PinSignInView( + Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + @StyleableRes + final int[] themeAttrs = { + R.attr.templateSignInMethodViewMaxWidth, + }; + + TypedArray ta = context.obtainStyledAttributes(themeAttrs); + mMaxWidth = ta.getDimensionPixelSize(0, 0); + ta.recycle(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); + if (mMaxWidth > 0 && mMaxWidth < measuredWidth) { + int measureMode = MeasureSpec.getMode(widthMeasureSpec); + widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, measureMode); + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mPinTextView = findViewById(R.id.pin_text); + } + /** Returns the maximum height of mPinTextView */ + public int getMaxLines() { + return mPinTextView.getMaxLines(); + } + + /** Sets the PIN text. */ + public void setText(CarUiText pinText) { + mPinTextView.setText(pinText); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/QRCodeSignInView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/QRCodeSignInView.java new file mode 100644 index 0000000..39d2aa7 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/QRCodeSignInView.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.drawable.BitmapDrawable; +import android.util.AttributeSet; +import android.widget.FrameLayout; +import android.widget.ImageView; +import androidx.car.app.model.signin.QRCodeSignInMethod; +import com.android.car.libraries.apphost.common.CarAppError; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.templates.host.R; +import com.google.zxing.WriterException; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import com.google.zxing.qrcode.encoder.ByteMatrix; +import com.google.zxing.qrcode.encoder.Encoder; +import com.google.zxing.qrcode.encoder.QRCode; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** A view that displays {@link QRCodeSignInMethod} UI. */ +public class QRCodeSignInView extends FrameLayout { + private ImageView mQRCodeView; + + public QRCodeSignInView(Context context) { + this(context, null); + } + + public QRCodeSignInView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public QRCodeSignInView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + @SuppressWarnings("ResourceType") + public QRCodeSignInView( + Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mQRCodeView = findViewById(R.id.qr_code_view); + } + + /** Sets the qr code. */ + public void setQRCodeSignInMethod( + TemplateContext templateContext, QRCodeSignInMethod qrCodeSignInMethod) { + setQRCode(templateContext, qrCodeSignInMethod.getUri().toString()); + } + + private void setQRCode(TemplateContext templateContext, String url) { + QRCode qrCode; + try { + qrCode = Encoder.encode(url, ErrorCorrectionLevel.H); + } catch (WriterException e) { + templateContext + .getErrorHandler() + .showError( + CarAppError.builder(templateContext.getCarAppPackageInfo().getComponentName()) + .setCause(e) + .build()); + return; + } + + BitmapDrawable drawable = new BitmapDrawable(getResources(), qrToBitmap(qrCode)); + drawable.setAntiAlias(false); + drawable.setFilterBitmap(false); + mQRCodeView.setImageDrawable(drawable); + } + + private Bitmap qrToBitmap(QRCode qrCode) { + ByteMatrix matrix = qrCode.getMatrix(); + int width = matrix.getWidth(); + int height = matrix.getHeight(); + int[] colors = new int[width * height]; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + colors[y * width + x] = (matrix.get(x, y) != 0) ? Color.WHITE : Color.TRANSPARENT; + } + } + + return Bitmap.createBitmap(colors, 0, width, width, height, Bitmap.Config.ALPHA_8); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowAdapter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowAdapter.java new file mode 100644 index 0000000..b040bc6 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowAdapter.java @@ -0,0 +1,1019 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import static com.android.car.libraries.apphost.template.view.model.RowListWrapper.LIST_FLAGS_TEMPLATE_HAS_LARGE_IMAGE; +import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId; +import static java.lang.Math.min; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; +import android.text.Spannable; +import android.text.SpannableString; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.ImageView; +import android.widget.RadioButton; +import android.widget.Switch; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StyleableRes; +import androidx.car.app.model.Action; +import androidx.car.app.model.CarColor; +import androidx.car.app.model.CarIcon; +import androidx.car.app.model.CarText; +import androidx.car.app.model.ForegroundCarColorSpan; +import androidx.car.app.model.Place; +import androidx.car.app.model.PlaceMarker; +import androidx.car.app.model.Row; +import com.android.car.libraries.apphost.common.CarColorUtils; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.distraction.constraints.CarColorConstraints; +import com.android.car.libraries.apphost.distraction.constraints.RowConstraints; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.template.view.model.RowListWrapper; +import com.android.car.libraries.apphost.template.view.model.RowWrapper; +import com.android.car.libraries.apphost.template.view.model.SelectionGroup; +import com.android.car.libraries.apphost.view.common.ActionButtonListParams; +import com.android.car.libraries.apphost.view.common.CarTextParams; +import com.android.car.libraries.apphost.view.common.CarTextUtils; +import com.android.car.libraries.apphost.view.common.ImageUtils; +import com.android.car.libraries.apphost.view.common.ImageViewParams; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.view.widgets.common.RowHolder.RowListener; +import com.android.car.ui.CarUiText; +import com.android.car.ui.recyclerview.CarUiContentListItem; +import com.android.car.ui.recyclerview.CarUiContentListItem.IconType; +import com.android.car.ui.recyclerview.CarUiListItem; +import com.android.car.ui.recyclerview.CarUiListItemAdapter; +import com.android.car.ui.widget.CarUiTextView; +import java.util.ArrayList; +import java.util.List; + +/** Adapter for {@link ContentView} to display {@link CarUiListItem}s. */ +public class RowAdapter extends CarUiListItemAdapter { + /** + * Start id for non-Chassis items. This value should be higher than any view type in {@link + * CarUiListItemAdapter}. + */ + private static final int ROW_LIST_VIEW_TYPE_BASE = 1000; + + /** + * Empty payload used with {@link #notifyItemChanged(int, Object)} to selectively disable item + * change animations. + */ + private static final Object EMPTY_ITEM_CHANGED_PAYLOAD = new Object(); + + private static final int ROW_LIST_VIEW_TYPE_ACTION_BUTTON = ROW_LIST_VIEW_TYPE_BASE + 1; + private static final int ROW_LIST_VIEW_TYPE_SECTION_HEADER = ROW_LIST_VIEW_TYPE_BASE + 2; + private static final int ROW_LIST_VIEW_TYPE_ROW = ROW_LIST_VIEW_TYPE_BASE + 3; + private static final CarColor SELECTED_TEXT_COLOR = CarColor.BLUE; + private static final int TITLE_MAX_LINE_COUNT = 2; + private static final int ONE_BODY_MAX_LINE_COUNT = 2; + private static final int MULTI_BODY_MAX_LINE_COUNT = 1; + private static final int MAX_IMAGES_PER_TEXT_LINE = 2; + + @ColorInt private final int mDefaultIconTint; + @Nullable private final Drawable mPlaceholderDrawable; + @Nullable private final Drawable mFullRowChevronDrawable; + @Nullable private final Drawable mHalfRowChevronDrawable; + @ColorInt private final int mRowBackgroundColor; + + private RowListener mRowListener; + private List<RowHolder> mRowHolders; + private TemplateContext mTemplateContext; + private final MarkerFactory mMarkerFactory; + private final boolean mUseCompactLayout; + private final int mTitleTextSize; + private final int mSecondaryTextSize; + private final int mSectionHeaderTextSize; + + static RowAdapter create( + Context context, + List<CarUiListItem> items, + MarkerFactory markerFactory, + boolean useCompactLayout) { + return new RowAdapter(context, items, markerFactory, useCompactLayout); + } + + private RowAdapter( + Context context, + List<CarUiListItem> items, + MarkerFactory markerFactory, + boolean useCompactLayout) { + super(items, useCompactLayout); + + mUseCompactLayout = useCompactLayout; + + @StyleableRes + final int[] themeAttrs = { + R.attr.templateRowDefaultIconTint, + R.attr.templateRowImagePlaceholder, + R.attr.templateFullRowChevronIcon, + R.attr.templateHalfRowChevronIcon, + R.attr.templateRowBackgroundColor, + }; + + TypedArray ta = context.obtainStyledAttributes(themeAttrs); + mDefaultIconTint = ta.getColor(0, 0); + mPlaceholderDrawable = ta.getDrawable(1); + mFullRowChevronDrawable = ta.getDrawable(2); + mHalfRowChevronDrawable = ta.getDrawable(3); + mRowBackgroundColor = ta.getColor(4, 0); + ta.recycle(); + + mTitleTextSize = getTextSizeFromAttribute(context, R.attr.templateRowTitleStyle); + mSecondaryTextSize = getTextSizeFromAttribute(context, R.attr.templateRowSecondaryTextStyle); + mSectionHeaderTextSize = + getTextSizeFromAttribute(context, R.attr.templateRowSectionHeaderStyle); + + mMarkerFactory = markerFactory; + } + + private static int getTextSizeFromAttribute(Context context, int attr) { + TypedArray ta = context.obtainStyledAttributes(new int[] {attr}); + int titleTextStyleResourceId = ta.getResourceId(0, 0); + ta.recycle(); + + ta = + context.obtainStyledAttributes( + titleTextStyleResourceId, new int[] {android.R.attr.textAppearance}); + int titleTextAppearanceResourceId = ta.getResourceId(0, 0); + ta.recycle(); + + ta = + context.obtainStyledAttributes( + titleTextAppearanceResourceId, new int[] {android.R.attr.textSize}); + int result = ta.getDimensionPixelSize(androidx.appcompat.R.styleable.TextAppearance_android_textSize, 0); + ta.recycle(); + + return result; + } + + public List<RowHolder> getRowHolders() { + return mRowHolders; + } + + /** Updates the rows of the adapter. */ + @SuppressWarnings("unchecked") + public void setRows( + TemplateContext templateContext, List<RowHolder> rowHolders, RowListener rowListener) { + int previousItemCount = mRowHolders == null ? 0 : mRowHolders.size(); + mTemplateContext = templateContext; + mRowHolders = rowHolders; + mRowListener = rowListener; + + List<CarUiListItem> items = new ArrayList<>(rowHolders.size()); + for (int index = 0; index < rowHolders.size(); index++) { + RowHolder holder = rowHolders.get(index); + RowWrapper rowWrapper = holder.getRowWrapper(); + CarUiListItem item = createCarUiListItem(templateContext, rowWrapper, index); + if (item == null) { + Log.e(LogTags.TEMPLATE, "Cannot create item for the row " + rowWrapper); + continue; + } + items.add(item); + } + + getItems().clear(); + ((List<CarUiListItem>) getItems()).addAll(items); + if (previousItemCount == items.size()) { + notifyItemRangeChanged(0, items.size(), EMPTY_ITEM_CHANGED_PAYLOAD); + } else { + notifyDataSetChanged(); + } + } + + /** Updates row at index {@code index} of the adapter. */ + @SuppressWarnings("unchecked") + public void updateRow(int index) { + if (index < 0 || index >= mRowHolders.size()) { + Log.e(LogTags.TEMPLATE, "Index out of bound " + index); + return; + } + RowWrapper rowWrapper = mRowHolders.get(index).getRowWrapper(); + CarUiListItem item = createCarUiListItem(mTemplateContext, rowWrapper, index); + if (item == null) { + Log.e(LogTags.TEMPLATE, "Cannot create item for the row " + rowWrapper); + return; + } + ((List<CarUiListItem>) getItems()).set(index, item); + // By passing a non-null payload to notifyItemChanged, we avoid ItemAnimator's onChange + // animation. This is needed when updating list items because otherwise the ViewHolder's + // contents flicker every time they are updated. + notifyItemChanged(index, EMPTY_ITEM_CHANGED_PAYLOAD); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if (viewType == ROW_LIST_VIEW_TYPE_ACTION_BUTTON) { + return new ActionButtonListViewHolder( + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.action_button_list_row, parent, false)); + } else if (viewType == ROW_LIST_VIEW_TYPE_SECTION_HEADER) { + return new SectionHeaderViewHolder( + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.row_section_header_view, parent, false)); + } else if (viewType == ROW_LIST_VIEW_TYPE_ROW) { + if (mUseCompactLayout) { + return new ListItemViewHolder( + LayoutInflater.from(parent.getContext()) + .inflate( + R.layout.half_list_row_view, /* root= */ parent, /* attachToRoot= */ false), + mHalfRowChevronDrawable); + } else { + return new ListItemViewHolder( + LayoutInflater.from(parent.getContext()) + .inflate( + R.layout.full_list_row_view, /* root= */ parent, /* attachToRoot= */ false), + mFullRowChevronDrawable); + } + } else { + return super.onCreateViewHolder(parent, viewType); + } + } + + @Override + public int getItemViewType(int position) { + if (getItems().get(position) instanceof ActionButtonListItem) { + return ROW_LIST_VIEW_TYPE_ACTION_BUTTON; + } else if (getItems().get(position) instanceof SectionHeaderItem) { + return ROW_LIST_VIEW_TYPE_SECTION_HEADER; + } else if (getItems().get(position) instanceof CarUiContentListItem) { + return ROW_LIST_VIEW_TYPE_ROW; + } else { + return super.getItemViewType(position); + } + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + if (holder.getItemViewType() == ROW_LIST_VIEW_TYPE_ACTION_BUTTON) { + CarUiListItem item = getItems().get(position); + if (!(item instanceof ActionButtonListItem)) { + Log.e(LogTags.TEMPLATE, "Incorrect item view type for item " + item); + return; + } + if (!(holder instanceof ActionButtonListViewHolder)) { + Log.e(LogTags.TEMPLATE, "Incorrect view holder type for list item."); + return; + } + + ((ActionButtonListViewHolder) holder).bind((ActionButtonListItem) item); + } else if (holder.getItemViewType() == ROW_LIST_VIEW_TYPE_SECTION_HEADER) { + CarUiListItem item = getItems().get(position); + if (!(item instanceof SectionHeaderItem)) { + Log.e(LogTags.TEMPLATE, "Incorrect item view type for item " + item); + return; + } + if (!(holder instanceof SectionHeaderViewHolder)) { + Log.e(LogTags.TEMPLATE, "Incorrect view holder type for section header item."); + return; + } + + ((SectionHeaderViewHolder) holder).bind((SectionHeaderItem) item); + } else if (holder.getItemViewType() == ROW_LIST_VIEW_TYPE_ROW) { + CarUiListItem item = getItems().get(position); + if (!(item instanceof CarUiContentListItem)) { + Log.e(LogTags.TEMPLATE, "Incorrect item view type for item " + item); + return; + } + if (!(holder instanceof ListItemViewHolder)) { + Log.e(LogTags.TEMPLATE, "Incorrect view holder type for row item " + item); + return; + } + + // TODO(b/205602000): investigate switching to ConstraintLayout for this instead of + // calculating the margin in code. + RowHolder rowHolder = getRowHolders().get(position); + boolean hasTemplateImageBesidesRow = + (rowHolder.getRowWrapper().getListFlags() & LIST_FLAGS_TEMPLATE_HAS_LARGE_IMAGE) != 0; + ((ListItemViewHolder) holder).bind((CarUiContentListItem) item, hasTemplateImageBesidesRow); + } else { + super.onBindViewHolder(holder, position); + } + } + + /** Converts a {@link Row} to a {@link CarUiListItem}. */ + @Nullable + public CarUiListItem createCarUiListItem( + TemplateContext templateContext, RowWrapper rowWrapper, int index) { + if (rowWrapper.getRow() instanceof Row) { + return createRowCarUiListItem(templateContext, rowWrapper, index); + } else if (ActionListUtils.isActionList(rowWrapper.getRow())) { + return createActionsCarUiListItem(templateContext, rowWrapper); + } else { + Log.i(LogTags.TEMPLATE, "Unknown row type ${rowWrapper.row.javaClass.name}"); + return null; + } + } + + /** Converts the {@link Row} to a header */ + private SectionHeaderItem createCarUiHeaderListItem(TemplateContext templateContext, Row row) { + CarTextParams params = + new CarTextParams.Builder() + .setMaxImages(MAX_IMAGES_PER_TEXT_LINE) + .setImageBoundingBox(new Rect(0, 0, mSectionHeaderTextSize, mSectionHeaderTextSize)) + .build(); + return new SectionHeaderItem( + CarTextUtils.toCharSequenceOrEmpty(templateContext, row.getTitle(), params)); + } + + @Nullable + private CarUiListItem createActionsCarUiListItem( + TemplateContext templateContext, RowWrapper rowWrapper) { + List<Action> actions = ActionListUtils.getActionList(rowWrapper.getRow()); + int maxActions = rowWrapper.getRowConstraints().getMaxActionsExclusive(); + + return new RowAdapter.ActionButtonListItem( + templateContext, actions, maxActions, mRowBackgroundColor); + } + + private CarUiListItem createRowCarUiListItem( + TemplateContext templateContext, RowWrapper rowWrapper, int index) { + Row row = (Row) rowWrapper.getRow(); + + // If this row is a header, create a header item instead + if ((rowWrapper.getRowFlags() & RowWrapper.ROW_FLAG_SECTION_HEADER) != 0) { + return createCarUiHeaderListItem(templateContext, row); + } + + CarUiContentListItem.Action action = createAction(rowWrapper); + CarUiContentListItem item = new CarUiContentListItem(action); + boolean colorContrastCheckPassed = + checkColorContrast(templateContext, row, mRowBackgroundColor); + updateItemText( + item, templateContext, rowWrapper, index, /* allowColor= */ colorContrastCheckPassed); + + // Only update the item image if there is no place marker. + if (!updateItemPlaceMarker(item, templateContext, rowWrapper)) { + updateItemImage( + item, templateContext, rowWrapper, index, /* allowTint= */ colorContrastCheckPassed); + } + updateCheckedState(item, rowWrapper, index); + updateActivationState(item, rowWrapper, index); + updateClickListener(item, rowWrapper, index); + + item.setOnCheckedChangeListener( + (v, checked) -> { + if (mRowListener != null) { + mRowListener.onCheckedChange(index); + } + }); + + return item; + } + + /** Checks the color contrast between contents of the given row and the background color. */ + private static boolean checkColorContrast( + TemplateContext templateContext, Row row, @ColorInt int backgroundColor) { + // Only the secondary texts can be colored, so check them + for (CarText carText : row.getTexts()) { + if (!CarTextUtils.checkColorContrast(templateContext, carText, backgroundColor)) { + return false; + } + } + + CarIcon image = row.getImage(); + if (image == null) { + return true; + } + CarColor tint = image.getTint(); + if (tint == null) { + return true; + } + + return CarColorUtils.checkColorContrast(templateContext, tint, backgroundColor); + } + + /** + * Sets the click listener if the row is actionable. An actionable row is one that has a click + * delegate, selection state, or toggle. + */ + private void updateClickListener(CarUiContentListItem item, RowWrapper rowWrapper, int index) { + Row row = (Row) rowWrapper.getRow(); + + boolean isClickable = + row.getOnClickDelegate() != null + && rowWrapper.getRowConstraints().isOnClickListenerAllowed(); + boolean isSelectable = rowWrapper.getSelectionGroup() != null; + boolean isToggle = row.getToggle() != null; + if (isClickable || isSelectable || isToggle) { + item.setOnItemClickedListener( + (v) -> { + if (mRowListener != null) { + mRowListener.onRowClicked(index); + } + }); + } else { + item.setOnItemClickedListener(null); + } + } + + /** Updates the text fields of the item using properties of the {@link RowWrapper}. */ + private void updateItemText( + CarUiContentListItem item, + TemplateContext templateContext, + RowWrapper rowWrapper, + int index, + boolean allowColor) { + Row row = (Row) rowWrapper.getRow(); + RowConstraints constraints = rowWrapper.getRowConstraints(); + int listFlags = rowWrapper.getListFlags(); + + boolean renderTitleAsSecondaryText = + (listFlags & RowListWrapper.LIST_FLAGS_RENDER_TITLE_AS_SECONDARY) != 0; + + // Create a copy because the row model returns unmodifiable list. + List<CarText> texts = new ArrayList<>(row.getTexts()); + + CarText title = row.getTitle(); + if (title != null) { + CarTextParams titleParams = + new CarTextParams.Builder() + .setMaxImages(MAX_IMAGES_PER_TEXT_LINE) + .setImageBoundingBox(new Rect(0, 0, mTitleTextSize, mTitleTextSize)) + .build(); + CharSequence titleString = + CarTextUtils.toCharSequenceOrEmpty(templateContext, title, titleParams); + if (titleString.length() > 0) { + if (renderTitleAsSecondaryText) { + texts.add(0, title); + } else { + item.setTitle( + CarUiTextUtils.fromCarText( + templateContext, title, titleParams, TITLE_MAX_LINE_COUNT)); + } + } + } + + int lineCount = texts.size(); + int maxLineCount = min(constraints.getMaxTextLinesPerRow(), lineCount); + + if (maxLineCount < lineCount) { + Log.d( + LogTags.TEMPLATE, + "Number of secondary text lines " + lineCount + " over limit of " + maxLineCount); + } + + while (!texts.isEmpty() && texts.size() > maxLineCount) { + texts.remove(texts.size() - 1); + } + + // Add selected text to the body if available. + CarText selectedText = createSelectedText(rowWrapper, index); + if (selectedText != null) { + texts.add(selectedText); + } + + if (!texts.isEmpty()) { + List<CarUiText> bodyTexts = createCarUiTextList(templateContext, texts, allowColor); + item.setBody(bodyTexts); + } + } + + /** + * Creates a {@link CarText} representing {@code selectedText} for the given {@link RowWrapper}. + * + * <p>Returns {@code null} if selected text is not available or the row is not selected. + */ + @Nullable + private CarText createSelectedText(RowWrapper rowWrapper, int index) { + CarText selectedText = rowWrapper.getSelectedText(); + if (selectedText == null) { + return null; + } + + SelectionGroup selectionGroup = rowWrapper.getSelectionGroup(); + if (selectionGroup == null || !selectionGroup.isSelected(index)) { + return null; + } + + SpannableString spannableSelectedText = new SpannableString(selectedText.toCharSequence()); + int start = 0; + int end = spannableSelectedText.length(); + spannableSelectedText.setSpan( + ForegroundCarColorSpan.create(SELECTED_TEXT_COLOR), + start, + end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + return new CarText.Builder(spannableSelectedText).build(); + } + + /** + * Updates the place marker image of the item using properties of the {@link RowWrapper}. + * + * <p>Returns true iff a place marker was found. + */ + private boolean updateItemPlaceMarker( + CarUiContentListItem item, TemplateContext templateContext, RowWrapper rowWrapper) { + Place place = rowWrapper.getMetadata().getPlace(); + if (place == null) { + return false; + } + + PlaceMarker marker = place.getMarker(); + if (marker == null) { + return false; + } + + Bitmap bitmap = mMarkerFactory.getContentForListMarker(templateContext, marker); + if (bitmap == null) { + return false; + } + + item.setPrimaryIconType(IconType.STANDARD); + item.setIcon(new BitmapDrawable(templateContext.getResources(), bitmap)); + return true; + } + + /** Updates the image of the item using properties of the {@link RowWrapper}. */ + private void updateItemImage( + CarUiContentListItem item, + TemplateContext templateContext, + RowWrapper rowWrapper, + int index, + boolean allowTint) { + Row row = (Row) rowWrapper.getRow(); + CarIcon image = row.getImage(); + if (image == null) { + return; + } + + CarUiContentListItem.IconType iconType = convertImageTypeToIconType(row.getRowImageType()); + if (iconType == null) { + Log.e(LogTags.TEMPLATE, "Unknown icon type for row " + row); + return; + } + item.setPrimaryIconType(iconType); + int iconSize = (int) getIconSize(iconType); + + ImageViewParams imageParams = + ImageViewParams.builder() + .setPlaceholderDrawable(mPlaceholderDrawable) + .setDefaultTint(mDefaultIconTint) + .setForceTinting(row.getRowImageType() == Row.IMAGE_TYPE_ICON) + .setIgnoreAppTint(!allowTint) + .setBackgroundColor(mRowBackgroundColor) + .setCarIconConstraints(rowWrapper.getRowConstraints().getCarIconConstraints()) + .build(); + ImageUtils.setImageTargetSrc( + templateContext, + row.getImage(), + drawable -> { + item.setIcon(drawable); + // By passing a non-null payload to notifyItemChanged, we avoid ItemAnimator's + // onChange animation. This is needed when updating list items because otherwise + // the ViewHolder's contents flicker every time they are updated. + notifyItemChanged(index, EMPTY_ITEM_CHANGED_PAYLOAD); + }, + imageParams, + iconSize, + iconSize); + } + + /** Updates the checked state of the item. */ + private void updateCheckedState(CarUiContentListItem item, RowWrapper rowWrapper, int index) { + SelectionGroup selectionGroup = rowWrapper.getSelectionGroup(); + boolean isSelected = selectionGroup != null && selectionGroup.getSelectedIndex() == index; + boolean hasRadioButton = item.getAction() == CarUiContentListItem.Action.RADIO_BUTTON; + + RowConstraints constraints = rowWrapper.getRowConstraints(); + boolean isToggleAllowed = constraints.isToggleAllowed(); + boolean hasToggle = item.getAction() == CarUiContentListItem.Action.SWITCH; + boolean isToggleChecked = rowWrapper.isToggleChecked(); + + item.setChecked( + (isSelected && hasRadioButton) || (isToggleAllowed && hasToggle && isToggleChecked)); + } + + /** Updates the activation state of the item. */ + private void updateActivationState(CarUiContentListItem item, RowWrapper rowWrapper, int index) { + SelectionGroup selectionGroup = rowWrapper.getSelectionGroup(); + boolean isSelected = selectionGroup != null && selectionGroup.getSelectedIndex() == index; + boolean useRadioButton = item.getAction() == CarUiContentListItem.Action.RADIO_BUTTON; + item.setActivated(isSelected && !useRadioButton && shouldHighlightSelectedRow(rowWrapper)); + } + + @Nullable + private static CarUiContentListItem.IconType convertImageTypeToIconType(int imageType) { + switch (imageType) { + case Row.IMAGE_TYPE_LARGE: + return IconType.CONTENT; + case Row.IMAGE_TYPE_SMALL: + case Row.IMAGE_TYPE_ICON: + return IconType.STANDARD; + default: + return null; + } + } + + private float getIconSize(CarUiContentListItem.IconType imageType) { + Resources res = mTemplateContext.getResources(); + switch (imageType) { + case CONTENT: + return res.getDimension(com.android.car.ui.R.dimen.car_ui_list_item_content_icon_width); + case STANDARD: + return res.getDimension(com.android.car.ui.R.dimen.car_ui_list_item_icon_size); + case AVATAR: + return res.getDimension(com.android.car.ui.R.dimen.car_ui_list_item_avatar_icon_width); + } + Log.e(LogTags.TEMPLATE, "Unknown imageType: " + imageType); + return res.getDimension(com.android.car.ui.R.dimen.car_ui_list_item_icon_size); + } + + /** Returns proper {@link CarUiContentListItem.Action} for a given {@link RowWrapper}. */ + private CarUiContentListItem.Action createAction(RowWrapper rowWrapper) { + if (!(rowWrapper.getRow() instanceof Row)) { + return CarUiContentListItem.Action.NONE; + } + + Row row = (Row) rowWrapper.getRow(); + if (row.isBrowsable()) { + return CarUiContentListItem.Action.CHEVRON; + } else if (row.getToggle() != null) { + return CarUiContentListItem.Action.SWITCH; + } else if (rowWrapper.getSelectionGroup() != null && shouldUseRadioButtons(rowWrapper)) { + return CarUiContentListItem.Action.RADIO_BUTTON; + } else { + return CarUiContentListItem.Action.NONE; + } + } + + /** + * Returns true if the flag for using radio buttons is enabled for the provided {@link + * RowWrapper}. + */ + private boolean shouldUseRadioButtons(RowWrapper rowWrapper) { + return (rowWrapper.getListFlags() & RowListWrapper.LIST_FLAGS_SELECTABLE_USE_RADIO_BUTTONS) + != 0; + } + + /** Returns true if the flag for highlighting the currently selected row is enabled */ + private boolean shouldHighlightSelectedRow(RowWrapper rowWrapper) { + return (rowWrapper.getListFlags() & RowListWrapper.LIST_FLAGS_SELECTABLE_HIGHLIGHT_ROW) != 0; + } + + /** Creates a list of {@link CarUiText} one for each given {@link CarText}. */ + private List<CarUiText> createCarUiTextList( + TemplateContext templateContext, List<CarText> carTexts, boolean allowColor) { + CarTextParams textParams = + CarTextParams.builder() + .setColorSpanConstraints( + allowColor ? CarColorConstraints.STANDARD_ONLY : CarColorConstraints.NO_COLOR) + .setMaxImages(MAX_IMAGES_PER_TEXT_LINE) + .setImageBoundingBox(new Rect(0, 0, mSecondaryTextSize, mSecondaryTextSize)) + .setBackgroundColor(mRowBackgroundColor) + .build(); + List<CarUiText> lines = new ArrayList<>(); + int maxLineCount = carTexts.size() > 1 ? MULTI_BODY_MAX_LINE_COUNT : ONE_BODY_MAX_LINE_COUNT; + for (CarText carText : carTexts) { + lines.add(CarUiTextUtils.fromCarText(templateContext, carText, textParams, maxLineCount)); + } + return lines; + } + + /** The {@link ViewHolder} for {@link ActionButtonListItem}. */ + static class ActionButtonListViewHolder extends RecyclerView.ViewHolder { + private final ActionButtonListView mActionButtonListView; + + ActionButtonListViewHolder(View view) { + super(view); + mActionButtonListView = requireViewByRefId(view, R.id.action_button_list_view); + } + + void bind(ActionButtonListItem item) { + mActionButtonListView.setActionList( + item.getTemplateContext(), + item.getActions(), + ActionButtonListParams.builder() + .setMaxActions(item.getMaxActions()) + .setOemReorderingAllowed(true) + .setOemColorOverrideAllowed(true) + .setSurroundingColor(item.getSurroundingColor()) + .build()); + } + } + + /** The {@link ViewHolder} for {@link SectionHeaderItem}. */ + static class SectionHeaderViewHolder extends RecyclerView.ViewHolder { + SectionHeaderViewHolder(View view) { + super(view); + } + + void bind(SectionHeaderItem item) { + CarUiTextView sectionHeaderView = (CarUiTextView) itemView; + sectionHeaderView.setText(item.getText()); + } + } + + /** View model for an {@link ActionButtonListView}. */ + public static class ActionButtonListItem extends CarUiListItem { + private final TemplateContext mTemplateContext; + private final List<Action> mActionList; + private final int mMaxActions; + @ColorInt private final int mSurroundingColor; + + ActionButtonListItem( + TemplateContext templateContext, + List<Action> actionList, + int maxActions, + @ColorInt int surroundingColor) { + mActionList = actionList; + mTemplateContext = templateContext; + mMaxActions = maxActions; + mSurroundingColor = surroundingColor; + } + + /** Returns a list of {@link Action}s */ + List<Action> getActions() { + return mActionList; + } + + /** Returns the associated {@link TemplateContext} */ + TemplateContext getTemplateContext() { + return mTemplateContext; + } + + /** Returns the maximum number of actions allowed. */ + int getMaxActions() { + return mMaxActions; + } + + /** Returns the color of the surrounding region around the action button list. */ + @ColorInt + int getSurroundingColor() { + return mSurroundingColor; + } + } + + /** View model for a section header. */ + public static class SectionHeaderItem extends CarUiListItem { + private final CharSequence mText; + + SectionHeaderItem(CharSequence text) { + mText = text; + } + + CharSequence getText() { + return mText; + } + } + + /** Holds views of {@link CarUiContentListItem}. */ + static class ListItemViewHolder extends RecyclerView.ViewHolder { + + final CarUiTextView mTitle; + final CarUiTextView mBody; + final ImageView mIcon; + final ImageView mContentIcon; + final ImageView mAvatarIcon; + final ViewGroup mIconContainer; + final ViewGroup mActionContainer; + final Switch mSwitch; + final CheckBox mCheckBox; + final RadioButton mRadioButton; + final ImageView mSupplementalIcon; + final View mTouchInterceptor; + final View mReducedTouchInterceptor; + final View mActionContainerTouchInterceptor; + @Nullable final Drawable mChevronDrawable; + @Nullable final View mLargeImageSpacer; + + ListItemViewHolder(@NonNull View itemView, @Nullable Drawable chevronDrawable) { + super(itemView); + mTitle = requireViewByRefId(itemView, R.id.car_ui_list_item_title); + mBody = requireViewByRefId(itemView, R.id.car_ui_list_item_body); + mIcon = requireViewByRefId(itemView, R.id.car_ui_list_item_icon); + mContentIcon = requireViewByRefId(itemView, R.id.car_ui_list_item_content_icon); + mAvatarIcon = requireViewByRefId(itemView, R.id.car_ui_list_item_avatar_icon); + mIconContainer = requireViewByRefId(itemView, R.id.car_ui_list_item_icon_container); + mActionContainer = requireViewByRefId(itemView, R.id.car_ui_list_item_action_container); + mSwitch = requireViewByRefId(itemView, R.id.car_ui_list_item_switch_widget); + mCheckBox = requireViewByRefId(itemView, R.id.car_ui_list_item_checkbox_widget); + mRadioButton = requireViewByRefId(itemView, R.id.car_ui_list_item_radio_button_widget); + mSupplementalIcon = requireViewByRefId(itemView, R.id.car_ui_list_item_supplemental_icon); + mReducedTouchInterceptor = + requireViewByRefId(itemView, R.id.car_ui_list_item_reduced_touch_interceptor); + mTouchInterceptor = requireViewByRefId(itemView, R.id.car_ui_list_item_touch_interceptor); + mActionContainerTouchInterceptor = + requireViewByRefId(itemView, R.id.car_ui_list_item_action_container_touch_interceptor); + mChevronDrawable = chevronDrawable; + mLargeImageSpacer = itemView.findViewById(R.id.large_image_spacer); + } + + void bind(@NonNull CarUiContentListItem item, boolean hasTemplateImageBesidesRow) { + CarUiText title = item.getTitle(); + if (title != null) { + mTitle.setText(title); + mTitle.setVisibility(View.VISIBLE); + } else { + mTitle.setVisibility(View.GONE); + } + + List<CarUiText> body = item.getBody(); + if (body != null) { + mBody.setText(body); + mBody.setVisibility(View.VISIBLE); + } else { + mBody.setVisibility(View.GONE); + } + + mIcon.setVisibility(View.GONE); + mContentIcon.setVisibility(View.GONE); + mAvatarIcon.setVisibility(View.GONE); + + Drawable icon = item.getIcon(); + if (icon != null) { + mIconContainer.setVisibility(View.VISIBLE); + + switch (item.getPrimaryIconType()) { + case CONTENT: + mContentIcon.setVisibility(View.VISIBLE); + mContentIcon.setImageDrawable(icon); + break; + case STANDARD: + mIcon.setVisibility(View.VISIBLE); + mIcon.setImageDrawable(icon); + break; + case AVATAR: + mAvatarIcon.setVisibility(View.VISIBLE); + mAvatarIcon.setImageDrawable(icon); + mAvatarIcon.setClipToOutline(true); + break; + } + } else { + mIconContainer.setVisibility(View.GONE); + } + + mSwitch.setVisibility(View.GONE); + mCheckBox.setVisibility(View.GONE); + mRadioButton.setVisibility(View.GONE); + mSupplementalIcon.setVisibility(View.GONE); + + CarUiContentListItem.OnClickListener itemOnClickListener = item.getOnClickListener(); + + switch (item.getAction()) { + case NONE: + mActionContainer.setVisibility(View.GONE); + + // Display ripple effects across entire item when clicked by using full-sized + // touch interceptor. + mTouchInterceptor.setVisibility(View.VISIBLE); + mTouchInterceptor.setOnClickListener( + v -> { + if (itemOnClickListener != null) { + itemOnClickListener.onClick(item); + } + }); + mTouchInterceptor.setClickable(itemOnClickListener != null); + mReducedTouchInterceptor.setVisibility(View.GONE); + mActionContainerTouchInterceptor.setVisibility(View.GONE); + break; + case SWITCH: + bindCompoundButton(item, mSwitch, itemOnClickListener); + break; + case CHECK_BOX: + bindCompoundButton(item, mCheckBox, itemOnClickListener); + break; + case RADIO_BUTTON: + bindCompoundButton(item, mRadioButton, itemOnClickListener); + break; + case CHEVRON: + mSupplementalIcon.setVisibility(View.VISIBLE); + mSupplementalIcon.setImageDrawable(mChevronDrawable); + mActionContainer.setVisibility(View.VISIBLE); + mTouchInterceptor.setVisibility(View.VISIBLE); + mTouchInterceptor.setOnClickListener( + v -> { + if (itemOnClickListener != null) { + itemOnClickListener.onClick(item); + } + }); + mTouchInterceptor.setClickable(itemOnClickListener != null); + mReducedTouchInterceptor.setVisibility(View.GONE); + mActionContainerTouchInterceptor.setVisibility(View.GONE); + break; + case ICON: + mSupplementalIcon.setVisibility(View.VISIBLE); + mSupplementalIcon.setImageDrawable(item.getSupplementalIcon()); + + mActionContainer.setVisibility(View.VISIBLE); + + // If the icon has a click listener, use a reduced touch interceptor to create + // two distinct touch area; the action container and the remainder of the list + // item. Each touch area will have its own ripple effect. If the icon has no + // click listener, it shouldn't be clickable. + if (item.getSupplementalIconOnClickListener() == null) { + mTouchInterceptor.setVisibility(View.VISIBLE); + mTouchInterceptor.setOnClickListener( + v -> { + if (itemOnClickListener != null) { + itemOnClickListener.onClick(item); + } + }); + mTouchInterceptor.setClickable(itemOnClickListener != null); + mReducedTouchInterceptor.setVisibility(View.GONE); + mActionContainerTouchInterceptor.setVisibility(View.GONE); + } else { + mReducedTouchInterceptor.setVisibility(View.VISIBLE); + mReducedTouchInterceptor.setOnClickListener( + v -> { + if (itemOnClickListener != null) { + itemOnClickListener.onClick(item); + } + }); + mReducedTouchInterceptor.setClickable(itemOnClickListener != null); + mActionContainerTouchInterceptor.setVisibility(View.VISIBLE); + mActionContainerTouchInterceptor.setOnClickListener( + (container) -> { + CarUiContentListItem.OnClickListener listener = + item.getSupplementalIconOnClickListener(); + if (listener != null) { + listener.onClick(item); + } + }); + mActionContainerTouchInterceptor.setClickable( + item.getSupplementalIconOnClickListener() != null); + mTouchInterceptor.setVisibility(View.GONE); + } + break; + } + + // Sets the right margin for the row to account for the space needed for the large image. + View spacer = mLargeImageSpacer; + if (spacer != null) { + spacer.setVisibility(hasTemplateImageBesidesRow ? View.VISIBLE : View.GONE); + } + + itemView.setActivated(item.isActivated()); + setEnabled(itemView, item.isEnabled()); + } + + void setEnabled(View view, boolean enabled) { + view.setEnabled(enabled); + if (view instanceof ViewGroup) { + ViewGroup group = (ViewGroup) view; + + for (int i = 0; i < group.getChildCount(); i++) { + setEnabled(group.getChildAt(i), enabled); + } + } + } + + void bindCompoundButton( + @NonNull CarUiContentListItem item, + @NonNull CompoundButton compoundButton, + @Nullable CarUiContentListItem.OnClickListener itemOnClickListener) { + compoundButton.setVisibility(View.VISIBLE); + compoundButton.setOnCheckedChangeListener(null); + compoundButton.setChecked(item.isChecked()); + compoundButton.setOnCheckedChangeListener( + (buttonView, isChecked) -> item.setChecked(isChecked)); + + // Clicks anywhere on the item should toggle the checkbox state. Use full touch + // interceptor. + mTouchInterceptor.setVisibility(View.VISIBLE); + mTouchInterceptor.setOnClickListener( + v -> { + compoundButton.toggle(); + if (itemOnClickListener != null) { + itemOnClickListener.onClick(item); + } + }); + // Compound button list items should always be clickable + mTouchInterceptor.setClickable(true); + mReducedTouchInterceptor.setVisibility(View.GONE); + mActionContainerTouchInterceptor.setVisibility(View.GONE); + + mActionContainer.setVisibility(View.VISIBLE); + mActionContainer.setClickable(false); + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowHolder.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowHolder.java new file mode 100644 index 0000000..8fcf34c --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowHolder.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import static com.android.car.libraries.apphost.template.view.model.RowWrapper.ROW_FLAG_SECTION_HEADER; +import static com.android.car.libraries.apphost.template.view.model.RowWrapper.ROW_FLAG_SHOW_DIVIDERS; +import static com.android.car.libraries.templates.host.view.widgets.common.ActionListUtils.isActionList; + +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.distraction.constraints.RowConstraints; +import com.android.car.libraries.apphost.distraction.constraints.RowListConstraints; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.template.view.model.RowWrapper; +import com.google.common.collect.ImmutableList; +import java.util.List; + +/** A holder of a row instance with its associated metadata. */ +public class RowHolder { + /** Listener of events related to a {@link RowHolder} instance. */ + public interface RowListener { + /** + * Notifies that a row has been selected. + * + * @param index index of the row in the list it belongs to. + */ + void onRowClicked(int index); + + /** + * Notifies that a row's check state has been changed. + * + * @param index index of the row in the list it belongs to. + */ + void onCheckedChange(int index); + + /** Notifies that a row's focus has changed. */ + void onRowFocusChanged(int index, boolean hasFocus); + } + + private final RowWrapper mRow; + + @Override + public String toString() { + return mRow.toString(); + } + + /** Creates a {@link RowHolder} from a row object. */ + public static RowHolder create(RowWrapper row) { + return new RowHolder(row); + } + + /** Returns a list of {@link RowHolder} instances from the given rows. */ + static ImmutableList<RowHolder> createHolders( + TemplateContext templateContext, List<RowWrapper> rows, RowListConstraints constraints) { + if (rows.isEmpty()) { + return ImmutableList.of(); + } + + ImmutableList.Builder<RowHolder> listBuilder = ImmutableList.builder(); + + int maxRowCount = + templateContext.getConstraintsProvider().getContentLimit(constraints.getListContentType()); + int nonHeaderRowCount = 0; + + // Cache the last seen header row, and only add it if there is a non-header row underneath + // it. + // We don't support consecutive header rows. + RowWrapper lastHeaderRow = null; + for (int i = 0; i < rows.size(); ++i) { + RowWrapper rowWrapper = rows.get(i); + Object rowObj = rowWrapper.getRow(); + + if (isActionList(rowObj)) { + // Special case for an action list which is only for the first row in PaneTemplate. + listBuilder.add(RowHolder.create(rowWrapper)); + } else { + // Ensure we only count the actual rows against the row limit. + boolean isSectionHeader = (rowWrapper.getRowFlags() & ROW_FLAG_SECTION_HEADER) != 0; + if (!isSectionHeader) { + nonHeaderRowCount++; + if (nonHeaderRowCount > maxRowCount) { + L.w( + LogTags.TEMPLATE, + "Row count exceeds the supported maximum of %d, will drop the" + + " remaining excess rows", + maxRowCount); + break; + } + + if (lastHeaderRow != null) { + listBuilder.add(RowHolder.create(lastHeaderRow)); + lastHeaderRow = null; + } + listBuilder.add(RowHolder.create(rowWrapper)); + } else { + if (lastHeaderRow != null) { + L.w( + LogTags.TEMPLATE, + "Consecutive header rows detected and is not supported, only the" + + " last one will be used"); + } + + lastHeaderRow = rowWrapper; + } + } + } + + return listBuilder.build(); + } + + public RowConstraints getConstraints() { + return mRow.getRowConstraints(); + } + + public RowWrapper getRowWrapper() { + return mRow; + } + + boolean isSectionHeader() { + return 0 != (mRow.getRowFlags() & ROW_FLAG_SECTION_HEADER); + } + + boolean showDividers() { + return 0 != (mRow.getRowFlags() & ROW_FLAG_SHOW_DIVIDERS); + } + + private RowHolder(RowWrapper row) { + mRow = row; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowListView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowListView.java new file mode 100644 index 0000000..6f393df --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowListView.java @@ -0,0 +1,577 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import static java.lang.Math.min; +import static java.util.Objects.requireNonNull; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.os.Build; +import android.os.Build.VERSION_CODES; + +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import androidx.annotation.Nullable; +import androidx.annotation.StyleableRes; +import androidx.annotation.VisibleForTesting; +import androidx.car.app.model.CarIcon; +import androidx.car.app.model.CarText; +import androidx.car.app.model.OnCheckedChangeDelegate; +import androidx.car.app.model.OnClickDelegate; +import androidx.car.app.model.OnItemVisibilityChangedDelegate; +import androidx.car.app.model.Place; +import androidx.car.app.model.Row; +import androidx.car.app.model.Toggle; +import androidx.recyclerview.widget.DefaultItemAnimator; +import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver; +import androidx.recyclerview.widget.RecyclerView.ItemAnimator; +import com.android.car.libraries.apphost.common.LocationMediator; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.distraction.constraints.RowListConstraints; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.logging.TelemetryEvent; +import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction; +import com.android.car.libraries.apphost.logging.TelemetryHandler; +import com.android.car.libraries.apphost.template.view.model.RowListWrapper; +import com.android.car.libraries.apphost.template.view.model.RowWrapper; +import com.android.car.libraries.apphost.template.view.model.SelectionGroup; +import com.android.car.libraries.apphost.view.common.ImageUtils; +import com.android.car.libraries.apphost.view.common.ImageViewParams; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.view.widgets.common.MarkerFactory.MarkerAppearance; +import com.android.car.libraries.templates.host.view.widgets.common.RowHolder.RowListener; +import com.android.car.ui.recyclerview.CarUiContentListItem; +import com.android.car.ui.recyclerview.CarUiListItem; +import com.android.car.ui.recyclerview.CarUiRecyclerView; +import com.android.car.ui.recyclerview.CarUiRecyclerView.OnScrollListener; +import com.android.car.ui.widget.CarUiTextView; +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; + +/** + * A view that can render a list of {@link androidx.car.app.model.Row} (wrapped inside a {@link + * RowListWrapper}. + */ +public class RowListView extends FrameLayout { + private final AdapterDataObserver mAdapterDataObserver = + new AdapterDataObserver() { + // call to update() not allowed on the given receiver. + @SuppressWarnings("nullness:method.invocation") + @Override + public void onChanged() { + super.onChanged(); + update(); + } + + @SuppressWarnings("nullness:method.invocation") + @Override + public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) { + super.onItemRangeChanged(positionStart, itemCount, payload); + update(); + } + + @SuppressWarnings("nullness:method.invocation") + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + super.onItemRangeMoved(fromPosition, toPosition, itemCount); + update(); + } + + @SuppressWarnings("nullness:method.invocation") + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + super.onItemRangeInserted(positionStart, itemCount); + update(); + } + }; + + private MarkerFactory mMarkerFactory; + private RowAdapter mListAdapter; + private RowVisibilityObserver mRowVisibilityObserver; + private ViewGroup mProgressContainer; + private CarUiTextView mEmptyListTextView; + private CarUiRecyclerView mListView; + private RowListWrapper mRowList; + private boolean mIsLoading; + private final boolean mUseCompactRowLayout; + + // This is only present in the full list view layout. + @Nullable private ViewGroup mLargeImageContainer; + + // This is only present in the full list view layout. + @Nullable private CarImageView mLargeImageView; + private final float mLargeImageWidthRatio; + private final int mLargeImageMaxWidth; + private final float mLargeImageAspectRatio; + private final int mRowListAndImagePadding; + private final int mLargeImageEndPadding; + private final int mLargeImageTopMargin; + private boolean hasLaidOutLargeImage; + + public RowListView(Context context) { + this(context, null); + } + + public RowListView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public RowListView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + @SuppressWarnings({"ResourceType", "nullness:method.invocation", "nullness:argument"}) + public RowListView( + Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + @StyleableRes + final int[] themeAttrs = { + R.attr.templateRowListToLargeImageRatio, + R.attr.templateRowListLargeImageContainerMaxWidth, + R.attr.templateRowListLargeImageAspectRatio, + R.attr.templateRowListAndImagePadding, + R.attr.templateFullRowEndPadding, + }; + TypedArray ta = context.obtainStyledAttributes(themeAttrs); + mLargeImageWidthRatio = ta.getFloat(0, 0.f); + int largeImageContainerMaxWidth = ta.getDimensionPixelSize(1, 0); + mLargeImageAspectRatio = ta.getFloat(2, 0.f); + mRowListAndImagePadding = ta.getDimensionPixelSize(3, 0); + // The end padding should be consistent with what was used for the row item's end padding. + mLargeImageEndPadding = ta.getDimensionPixelSize(4, 0); + ta.recycle(); + mLargeImageMaxWidth = + largeImageContainerMaxWidth - (mRowListAndImagePadding + mLargeImageEndPadding); + mLargeImageTopMargin = context.getResources().getDimensionPixelSize(R.dimen.template_padding_2); + + ta = context.obtainStyledAttributes(attrs, R.styleable.RowListView, defStyleAttr, defStyleRes); + mUseCompactRowLayout = ta.getBoolean(R.styleable.RowListView_listUseCompactRowLayout, false); + ta.recycle(); + + mMarkerFactory = + MarkerFactory.create( + context, new MarkerAppearance(context, attrs, defStyleAttr, defStyleAttr)); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mProgressContainer = findViewById(R.id.progress_container); + mEmptyListTextView = findViewById(R.id.list_no_items_text); + mListView = findViewById(R.id.list_view); + mRowVisibilityObserver = RowVisibilityObserver.create(requireNonNull(mListView)); + mListAdapter = + RowAdapter.create(getContext(), new ArrayList<>(), mMarkerFactory, mUseCompactRowLayout); + mListView.setAdapter(mListAdapter); + + // TODO(b/210167386): setItemAnimator will deprecate for sc+. We can still use the code below to + // control the itemAnimator for qt and rvc + if (Build.VERSION.SDK_INT <= VERSION_CODES.R) { + ItemAnimator itemAnimatorNoDuration = new DefaultItemAnimator(); + itemAnimatorNoDuration.setAddDuration(0); + itemAnimatorNoDuration.setChangeDuration(0); + itemAnimatorNoDuration.setMoveDuration(0); + itemAnimatorNoDuration.setRemoveDuration(0); + mListView.setItemAnimator(itemAnimatorNoDuration); + } + + mListAdapter.registerAdapterDataObserver(mAdapterDataObserver); + + ViewGroup imageViewContainer = findViewById(R.id.large_image_container); + if (imageViewContainer != null) { + mLargeImageContainer = imageViewContainer; + mLargeImageView = imageViewContainer.findViewById(R.id.large_image); + + // Synchronize the scrolling of the list with the vertical offset of the image. + mListView.addOnScrollListener( + new OnScrollListener() { + @Override + public void onScrolled(CarUiRecyclerView recyclerView, int dx, int dy) { + FrameLayout.LayoutParams layoutParams = + (FrameLayout.LayoutParams) imageViewContainer.getLayoutParams(); + layoutParams.topMargin -= dy; + imageViewContainer.setLayoutParams(layoutParams); + } + + @Override + public void onScrollStateChanged(CarUiRecyclerView recyclerView, int newState) { + // No-op. + } + }); + } + + update(); + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public CarUiRecyclerView getRecyclerView() { + return mListView; + } + + @Nullable + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public RowAdapter getAdapter() { + return mListAdapter; + } + + void setRowList(TemplateContext templateContext, RowListWrapper rowList) { + mRowList = rowList; + + boolean isLoading = rowList.isLoading(); + if (mIsLoading != isLoading) { + // Trigger a visibility update if the loading state has changed. + mIsLoading = isLoading; + update(); + + if (mIsLoading) { + // Scroll to the top so we will show the first row when the loading finishes. + mListView.scrollToPosition(0); + } + } + + CarText emptyListCarText = rowList.getEmptyListText(); + CharSequence emptyText; + if (emptyListCarText != null && !emptyListCarText.isEmpty()) { + mEmptyListTextView.setText( + CarUiTextUtils.fromCarText( + templateContext, emptyListCarText, mEmptyListTextView.getMaxLines())); + } else { + emptyText = + templateContext.getText( + templateContext.getHostResourceIds().getTemplateListNoItemsText()); + mEmptyListTextView.setText( + CarUiTextUtils.fromCharSequence( + templateContext, emptyText, mEmptyListTextView.getMaxLines())); + } + + CarIcon paneImage = rowList.getImage(); + ViewGroup imageViewContainer = mLargeImageContainer; + if (imageViewContainer != null) { + if (paneImage != null) { + ImageUtils.setImageSrc( + templateContext, paneImage, requireNonNull(mLargeImageView), ImageViewParams.DEFAULT); + } + } + + RowListConstraints constraints = rowList.getRowListConstraints(); + List<RowHolder> rowHolders = + RowHolder.createHolders(templateContext, rowList.getRowWrappers(), constraints); + + mRowVisibilityObserver.setOnItemVisibilityChangedListener( + (startIndexInclusive, endIndexExclusive) -> { + OnItemVisibilityChangedDelegate onItemVisibilityChangedDelegate = + rowList.getOnItemVisibilityChangedDelegate(); + if (onItemVisibilityChangedDelegate != null) { + templateContext + .getAppDispatcher() + .dispatchItemVisibilityChanged( + onItemVisibilityChangedDelegate, startIndexInclusive, endIndexExclusive); + } + publishMetadata( + templateContext, rowList.getRowWrappers(), startIndexInclusive, endIndexExclusive); + }); + + mListAdapter.setRows( + templateContext, + rowHolders, + new RowListener() { + @Override + public void onRowClicked(int index) { + TelemetryHandler telemetry = templateContext.getTelemetryHandler(); + telemetry.logCarAppTelemetry( + TelemetryEvent.newBuilder(TelemetryEvent.UiAction.ROW_CLICKED).setPosition(index)); + + onRowSelected(templateContext, index, /* clicked= */ true); + } + + @Override + public void onCheckedChange(int index) { + TelemetryHandler telemetry = templateContext.getTelemetryHandler(); + telemetry.logCarAppTelemetry( + TelemetryEvent.newBuilder(TelemetryEvent.UiAction.ROW_CLICKED).setPosition(index)); + + maybeSwitchToggleState(templateContext, index); + } + + @Override + public void onRowFocusChanged(int index, boolean hasFocus) { + RowListView.this.onRowFocused(templateContext, index, hasFocus); + } + }); + + if (!rowList.isRefresh()) { + mListView.scrollToPosition(0); + } + + ViewUtils.logCarAppTelemetry(templateContext, UiAction.LIST_SIZE, rowHolders.size()); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + // The layout of the large image container is dependent on the final size and paddings of the + // list items insider the RecyclerView. Here we obtain the bounds of the first item in the + // RecyclerView and lines up the image container based on that. + // + // We only need to do this once at the beginning to place the image at the right position. + // Subsequent synchronization is handled via the OnScrollListener. + ViewGroup imageViewContainer = mLargeImageContainer; + if (imageViewContainer == null || hasLaidOutLargeImage) { + return; + } + + int firstActualRowIndex = -1; + List<? extends CarUiListItem> items = mListAdapter.getItems(); + // Find the first item that is a CarUiContentListItem which is used for an actual Row. + // Action button lists and section headers use different CarUiListItem types. + for (int i = 0; i < items.size(); i++) { + CarUiListItem item = items.get(i); + if (item instanceof CarUiContentListItem) { + firstActualRowIndex = i; + break; + } + } + + if (firstActualRowIndex == -1) { + return; + } + + View itemView = mListView.getRecyclerViewChildAt(firstActualRowIndex); + if (itemView != null) { + // Get the item view bounds relative to the RowListView container, and use that + // to determine the offset for the image view. + Rect itemViewBound = new Rect(); + itemView.getDrawingRect(itemViewBound); + RowListView.this.offsetDescendantRectToMyCoords(itemView, itemViewBound); + + // Sets the bounding box based on desired width and aspect ratio. + int imageWidth = + min( + mLargeImageMaxWidth, + // Image width is a ratio of the total container width, accounting for the + // padding we want from the row and the edge of the screen. + (int) (mLargeImageWidthRatio * itemViewBound.width()) + - (mRowListAndImagePadding + mLargeImageEndPadding)); + + FrameLayout.LayoutParams imageParams = + (FrameLayout.LayoutParams) imageViewContainer.getLayoutParams(); + imageParams.topMargin = itemViewBound.top + mLargeImageTopMargin; + imageParams.setMarginEnd( + RowListView.this.getRight() - itemViewBound.right + mLargeImageEndPadding); + imageParams.width = imageWidth; + imageParams.height = (int) (imageWidth * mLargeImageAspectRatio); + imageViewContainer.setLayoutParams(imageParams); + + hasLaidOutLargeImage = true; + } + } + + private void onRowSelected(TemplateContext templateContext, int newIndex, boolean clicked) { + RowWrapper rowWrapper = getRowWrapperIfValid(newIndex); + if (rowWrapper == null) { + return; + } + + if (rowWrapper.getRow() instanceof Row) { + Row row = (Row) rowWrapper.getRow(); + final OnClickDelegate onClickDelegate = row.getOnClickDelegate(); + if (onClickDelegate != null) { + templateContext.getAppDispatcher().dispatchClick(onClickDelegate); + } + } + + SelectionGroup selectionGroup = rowWrapper.getSelectionGroup(); + + // If the row belongs to a selection group, change the selection in the group if necessary. + // This is done here in the host without having to do a round-trip to the client to change + // the + // model and re-fresh the entire list, which is much faster and more convenient for apps. + if (selectionGroup != null) { + int currentSelectionIndex = selectionGroup.getSelectedIndex(); + + // If the selected index changed, deselect the previously selected row, and select the + // new + // one. + boolean isRowPreviouslySelected = currentSelectionIndex == newIndex; + if (!isRowPreviouslySelected) { + // Store the new selection. This is important also in case the rows get re-created + // during recycling so that they maintain the proper state. + selectionGroup.setSelectedIndex(newIndex); + + mListAdapter.updateRow(currentSelectionIndex); + mListAdapter.updateRow(newIndex); + + boolean shouldScrollToSelectedRow = + (mRowList.getListFlags() & RowListWrapper.LIST_FLAGS_SELECTABLE_SCROLL_TO_ROW) != 0; + if (shouldScrollToSelectedRow) { + // Post to the main thread so that the scroll happens after the UI changes for + // the selected state is completed. + post(() -> mListView.smoothScrollToPosition(newIndex)); + } + } + + // Dispatch the selection callbacks. + // Note the selection event is dispatched regardless of selection index actually + // changing. + templateContext + .getAppDispatcher() + .dispatchSelected( + selectionGroup.getOnSelectedDelegate(), selectionGroup.getRelativeIndex(newIndex)); + if (isRowPreviouslySelected && clicked) { + Runnable runnable = mRowList.getRepeatedSelectionCallback(); + if (runnable != null) { + runnable.run(); + } + } + } + } + + private void onRowFocused(TemplateContext templateContext, int index, boolean hasFocus) { + // Select the row if moving the focus should change the selection, and we have a new focus. + boolean focusChangeSelection = + (mRowList.getListFlags() & RowListWrapper.LIST_FLAGS_SELECTABLE_FOCUS_SELECT_ROW) != 0; + if (focusChangeSelection && hasFocus) { + onRowSelected(templateContext, index, /* clicked= */ false); + } + } + + /** Switches the toggle state of a row if it does contain a toggle. */ + private void maybeSwitchToggleState(TemplateContext templateContext, int index) { + RowWrapper rowWrapper = getRowWrapperIfValid(index); + if (rowWrapper == null) { + return; + } + + Object rowObj = rowWrapper.getRow(); + + // Only rows can contain toggles. + if (rowObj instanceof Row) { + Row row = (Row) rowObj; + + // Does the row contain a toggle ? if so, switch its state. + Toggle toggle = row.getToggle(); + if (toggle != null) { + rowWrapper.switchToggleState(); + // Dispatch the checked change callback to the app. + OnCheckedChangeDelegate delegate = toggle.getOnCheckedChangeDelegate(); + templateContext + .getAppDispatcher() + .dispatchCheckedChanged(delegate, rowWrapper.isToggleChecked()); + } + } + } + + private void update() { + boolean isLoading = mIsLoading; + ViewGroup largeImageContainer = mLargeImageContainer; + if (isLoading) { + mProgressContainer.setVisibility(VISIBLE); + + // Mark the content views as invisible so that the size of the container remains the + // same while the progress bar is showing. + mEmptyListTextView.setVisibility(INVISIBLE); + mListView.setVisibility(INVISIBLE); + + if (largeImageContainer != null) { + largeImageContainer.setVisibility(INVISIBLE); + } + + return; + } + + mProgressContainer.setVisibility(GONE); + + // If the list is empty, hide it and display a message instead. + boolean isEmpty = mListAdapter.getItemCount() == 0; + if (isEmpty) { + mEmptyListTextView.setVisibility(VISIBLE); + mListView.setVisibility(GONE); + + if (largeImageContainer != null) { + largeImageContainer.setVisibility(GONE); + } + + mEmptyListTextView.setFocusable(true); + } else { + mEmptyListTextView.setVisibility(GONE); + mListView.setVisibility(VISIBLE); + + if (largeImageContainer != null) { + largeImageContainer.setVisibility(mRowList.getImage() != null ? VISIBLE : GONE); + } + } + } + + /** Publish any non-null {@link Place}s from the list of {@link RowWrapper}. */ + private void publishMetadata( + TemplateContext templateContext, + List<RowWrapper> rowWrappers, + int startIndexInclusive, + int endIndexExclusive) { + if (templateContext == null) { + L.e(LogTags.TEMPLATE, "TemplateContext is null"); + return; + } + + // Return if the range is empty. + if (startIndexInclusive < 0) { + return; + } + + if (endIndexExclusive > rowWrappers.size()) { + L.e(LogTags.TEMPLATE, "Index out of bound: (%d > %d)", endIndexExclusive, rowWrappers.size()); + return; + } + + ImmutableList.Builder<Place> builder = new ImmutableList.Builder<>(); + for (int index = startIndexInclusive; index < endIndexExclusive; index++) { + RowWrapper rowWrapper = rowWrappers.get(index); + Place place = rowWrapper.getMetadata().getPlace(); + if (place != null) { + builder.add(place); + } + } + + ImmutableList<Place> places = builder.build(); + L.v(LogTags.TEMPLATE, "Updating %d visible places", places.size()); + requireNonNull(templateContext.getAppHostService(LocationMediator.class)) + .setCurrentPlaces(places); + } + + @Nullable + private RowWrapper getRowWrapperIfValid(int index) { + // The user may click on a row that is transitioning out and the index here may be invalid + // for the new rows being transitioned in. Ignore those cases. + // Theoretically this means that we may trigger a click on a new row that was + // not clicked on (e.g. if the user double-taps really fast on the previously row), but that + // seems like a low-probability scenario in real HU so we are not doing extra checks here. + List<RowWrapper> rowWrappers = mRowList.getRowWrappers(); + if (index >= rowWrappers.size()) { + return null; + } + + return rowWrappers.get(index); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowVisibilityObserver.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowVisibilityObserver.java new file mode 100644 index 0000000..eda9af1 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowVisibilityObserver.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import static java.util.Objects.requireNonNull; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.LayoutManager; +import android.view.View; +import android.view.View.OnLayoutChangeListener; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.ui.recyclerview.CarUiRecyclerView; +import com.android.car.ui.recyclerview.CarUiRecyclerView.OnScrollListener; + +/** + * Observes the visibility change of the given {@link RecyclerView}. + * + * <p>Since we do not own the layout manager of {@link RecyclerView}, this class can be used to + * listen for the visibility changes of the recycler view due to scrolling. + */ +public class RowVisibilityObserver { + + /** Listener for the item visibility changes. */ + interface OnItemVisibilityChangedListener { + + /** Callback when item visibility changes. */ + void sendItemVisibilityChanged(int startIndexInclusive, int endIndexExclusive); + } + + private static final int MSG_HANDLE_VISIBLE_ROWS_CHANGE = 1; + private static final int HANDLE_ROW_CHANGE_DELAY_MILLIS = 150; + private static final int INVALID_ROW_INDEX = Integer.MIN_VALUE; + + @NonNull private final CarUiRecyclerView mRecyclerView; + private final Handler mHandler = new Handler(Looper.getMainLooper(), new HandlerCallback()); + private final OnScrollListener mOnScrollListener = + new CarUiRecyclerView.OnScrollListener() { + // Suppressing error for referencing handleVisibleRowsChange() before + // initialization of RowVisibilityObserver. + @SuppressWarnings("nullness:method.invocation") + @Override + public void onScrollStateChanged(CarUiRecyclerView recyclerView, int newState) { + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + handleVisibleRowsChange(); + } + } + + @Override + public void onScrolled(CarUiRecyclerView recyclerView, int dx, int dy) {} + }; + + private final OnLayoutChangeListener mOnLayoutChangeListener = + new OnLayoutChangeListener() { + // Suppressing error for referencing handleVisibleRowsChange() before + // initialization of RowVisibilityObserver and mReceyclerView being null. + @SuppressWarnings("nullness") + @Override + public void onLayoutChange( + View v, + int left, + int top, + int right, + int bottom, + int oldLeft, + int oldTop, + int oldRight, + int oldBottom) { + if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE) { + handleVisibleRowsChange(); + } + } + }; + + @Nullable private OnItemVisibilityChangedListener mListener; + private int mFirstVisibleRowIndex; + private int mLastVisibleRowIndexExclusive; + + /** Returns an instance of {@link RowVisibilityObserver}. */ + public static RowVisibilityObserver create(@NonNull CarUiRecyclerView recyclerView) { + return new RowVisibilityObserver(requireNonNull(recyclerView)); + } + + /** Sets an {@link OnItemVisibilityChangedListener}. */ + public void setOnItemVisibilityChangedListener( + @NonNull OnItemVisibilityChangedListener listener) { + requireNonNull(listener); + + // Remove any existing listener. + removeOnItemVisibilityChangedListener(); + + mListener = listener; + + // Reset the cached start/end indices, so that the newly-set listener will be invoked even + // if the visible rows might not have changed. + mFirstVisibleRowIndex = INVALID_ROW_INDEX; + mLastVisibleRowIndexExclusive = INVALID_ROW_INDEX; + + mRecyclerView.addOnScrollListener(mOnScrollListener); + mRecyclerView.addOnLayoutChangeListener(mOnLayoutChangeListener); + } + + /** Removes any existing {@link OnItemVisibilityChangedListener}. */ + public void removeOnItemVisibilityChangedListener() { + if (mListener == null) { + return; + } + + mRecyclerView.removeOnScrollListener(mOnScrollListener); + mRecyclerView.removeOnLayoutChangeListener(mOnLayoutChangeListener); + mListener = null; + } + + /** Creates a {@link RowVisibilityObserver} for given {@link RecyclerView}. */ + private RowVisibilityObserver(@NonNull CarUiRecyclerView recyclerView) { + mRecyclerView = requireNonNull(recyclerView); + + // Start with an invalid index, so that the newly-set listener will be invoked. + mFirstVisibleRowIndex = INVALID_ROW_INDEX; + mLastVisibleRowIndexExclusive = INVALID_ROW_INDEX; + } + + + /** Sends a message to the handler to publish item visibility change event. */ + private void handleVisibleRowsChange() { + mHandler.removeMessages(MSG_HANDLE_VISIBLE_ROWS_CHANGE); + Message message = mHandler.obtainMessage(MSG_HANDLE_VISIBLE_ROWS_CHANGE); + if (mRecyclerView.getRecyclerViewChildCount() == 0) { + // When a full data refresh happens in the adapter that backs the recycler view, the + // view reports no visible items first for a few milliseconds, and then reports the new + // updated items. + // This ephemeral state of emptiness can cause flickering for the views that listen to + // the published events (e.g. the map view which clears and renders pins in the map). + // This is a work around by adding a short delay before sending the item visibility + // change event. + // TODO(b/183989613): Possibly remove once list diffing is implemented. + mHandler.sendMessageDelayed(message, HANDLE_ROW_CHANGE_DELAY_MILLIS); + } else { + mHandler.sendMessage(message); + } + } + + /** A {@link Handler.Callback} used to process the message queue for the visibility events. */ + private class HandlerCallback implements Handler.Callback { + + /** Publishes the item visibility changed event to the listener. */ + @Override + public boolean handleMessage(Message msg) { + if (msg.what == MSG_HANDLE_VISIBLE_ROWS_CHANGE) { + int firstVisibleRowIndex = mRecyclerView.findFirstCompletelyVisibleItemPosition(); + int lastVisibleRowIndex = mRecyclerView.findLastCompletelyVisibleItemPosition(); + int lastVisibleRowIndexExclusive = lastVisibleRowIndex + 1; + + L.d( + LogTags.TEMPLATE, + "Handling visible rows in range (%d, %d)", + firstVisibleRowIndex, + lastVisibleRowIndexExclusive); + + if (firstVisibleRowIndex == mFirstVisibleRowIndex + && lastVisibleRowIndexExclusive == mLastVisibleRowIndexExclusive) { + return true; + } + + if (mListener != null) { + mListener.sendItemVisibilityChanged(firstVisibleRowIndex, lastVisibleRowIndexExclusive); + } + + mFirstVisibleRowIndex = firstVisibleRowIndex; + mLastVisibleRowIndexExclusive = lastVisibleRowIndexExclusive; + + return true; + } else { + L.w(LogTags.TEMPLATE, "Unknown message: %s", msg); + } + return false; + } + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/SearchHeaderView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/SearchHeaderView.java new file mode 100644 index 0000000..4154695 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/SearchHeaderView.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import static com.google.common.base.Strings.nullToEmpty; + +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.widget.EditText; +import androidx.annotation.Nullable; +import androidx.car.app.model.Action; +import androidx.car.app.model.ActionStrip; +import androidx.car.app.model.SearchCallbackDelegate; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints; +import com.android.car.libraries.apphost.input.InputManager; +import com.android.car.ui.core.CarUi; +import com.android.car.ui.toolbar.SearchMode; +import com.android.car.ui.toolbar.ToolbarController; + +/** A view that displays the header for the search templates. */ +public class SearchHeaderView extends AbstractHeaderView { + private final CarEditTextWrapper mEditableSearchBar; + private final EditText mSearchBar; + + private SearchHeaderView( + TemplateContext templateContext, + ToolbarController toolbarController, + View rootView, + @Nullable String initialSearchText, + @Nullable SearchCallbackDelegate searchCallbackDelegate, + boolean keyboardOpened) { + super(templateContext, toolbarController); + + InputManager mInputManager = templateContext.getInputManager(); + mToolbarController.setSearchMode(SearchMode.SEARCH); + mSearchBar = rootView.requireViewById(com.android.car.ui.R.id.car_ui_toolbar_search_bar); + mEditableSearchBar = new CarEditTextWrapper(mSearchBar, mInputManager); + + toolbarController.setSearchQuery(nullToEmpty(initialSearchText)); + + if (searchCallbackDelegate != null) { + mToolbarController.registerOnSearchListener( + query -> + templateContext + .getAppDispatcher() + .dispatchSearchTextChanged(searchCallbackDelegate, query)); + + toolbarController.registerOnSearchCompletedListener( + () -> { + String query = mSearchBar.getText().toString(); + templateContext + .getAppDispatcher() + .dispatchSearchSubmitted(searchCallbackDelegate, query); + }); + } + + if (keyboardOpened) { + mInputManager.startInput(mEditableSearchBar); + } + + // TODO(b/179220417): Handle disabling search while driving + } + + /** Returns the searchBar of the header */ + public EditText getSearchBar() { + return mSearchBar; + } + + /** Returns the {@link InputConnection} for the search bar. */ + public InputConnection onCreateInputConnection(EditorInfo editorInfo) { + return mEditableSearchBar.onCreateInputConnection(editorInfo); + } + + /** Updates the optional button in the header. */ + @Override + public void setAction(@Nullable Action action) { + super.setAction(action); + } + + /** Updates the {@link ActionStrip} associated with this toolbar */ + @Override + public void setActionStrip(@Nullable ActionStrip actionStrip, ActionsConstraints constraints) { + super.setActionStrip(actionStrip, constraints); + boolean hasMenuItems = actionStrip != null && !actionStrip.getActions().isEmpty(); + mToolbarController.setShowMenuItemsWhileSearching(hasMenuItems); + } + + /** Updates the search hint. */ + public void setHint(@Nullable String searchHint) { + mToolbarController.setSearchHint(searchHint != null ? searchHint : ""); + } + + /** Installs a {@link HeaderView} around the given container view */ + @SuppressWarnings("nullness:argument") // InsetsChangedListener is nullable. + public static SearchHeaderView install( + TemplateContext templateContext, + View container, + View rootView, + @Nullable String initialSearchText, + @Nullable SearchCallbackDelegate searchCallbackDelegate, + boolean keyboardOpened) { + ToolbarController toolbarController = CarUi.installBaseLayoutAround(container, null, true); + if (toolbarController == null) { + throw new NullPointerException("Toolbar Controller could not be created."); + } + return new SearchHeaderView( + templateContext, + toolbarController, + rootView, + initialSearchText, + searchCallbackDelegate, + keyboardOpened); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/StringReplacementSpan.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/StringReplacementSpan.java new file mode 100644 index 0000000..b93aea3 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/StringReplacementSpan.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.FontMetricsInt; +import android.graphics.Rect; +import android.text.style.ReplacementSpan; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +/** + * A simple class for replacement the text that this span is attached to with the given replacement. + */ +public class StringReplacementSpan extends ReplacementSpan { + + private final String mReplacementText; + + public StringReplacementSpan(String text) { + mReplacementText = text; + } + + /** Returns the replacement string for replacing the attached text. */ + @VisibleForTesting + public String getReplacementText() { + return mReplacementText; + } + + @Override + public int getSize( + Paint paint, CharSequence text, int start, int end, @Nullable FontMetricsInt fm) { + Rect bounds = new Rect(); + paint.getTextBounds(mReplacementText, 0, mReplacementText.length(), bounds); + return bounds.width(); + } + + @Override + public void draw( + Canvas canvas, + CharSequence text, + int start, + int end, + float x, + int top, + int y, + int bottom, + Paint paint) { + canvas.drawText(mReplacementText, x, y, paint); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ViewUtils.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ViewUtils.java new file mode 100644 index 0000000..a9a98d4 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ViewUtils.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common; + +import static java.lang.Math.max; +import static java.lang.Math.min; + +import android.graphics.Rect; +import android.view.MotionEvent; +import android.view.TouchDelegate; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import androidx.annotation.Nullable; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.logging.TelemetryEvent; +import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction; +import com.android.car.libraries.apphost.logging.TelemetryHandler; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +/** Utility class for view operations. */ +public class ViewUtils { + private ViewUtils() {} + + /** A {@link TouchDelegate} that allows combining multiple {@link TouchDelegate}s into one */ + private static class TouchDelegateComposite extends TouchDelegate { + private static class TouchDelegateInfo { + final TouchDelegate mTouchDelegate; + @Nullable final WeakReference<View> mTargetView; + + TouchDelegateInfo(TouchDelegate touchDelegate, @Nullable View targetView) { + mTouchDelegate = touchDelegate; + mTargetView = targetView != null ? new WeakReference<>(targetView) : null; + } + } + + private final List<TouchDelegateInfo> delegates = new ArrayList<>(); + + private static final Rect emptyRect = new Rect(); + + public TouchDelegateComposite(View view) { + super(emptyRect, view); + } + + public void addDelegate(TouchDelegate delegate, @Nullable View targetView) { + if (delegate != null) { + delegates.add(new TouchDelegateInfo(delegate, targetView)); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + boolean res = false; + float x = event.getX(); + float y = event.getY(); + for (TouchDelegateInfo delegateInfo : delegates) { + event.setLocation(x, y); + if (delegateInfo.mTargetView != null && delegateInfo.mTargetView.get() == null) { + throw new IllegalStateException("Invalid touch delegation, target view has be removed"); + } + res = delegateInfo.mTouchDelegate.onTouchEvent(event) || res; + } + return res; + } + } + + // Returns true if {@code child} is a descendant of {@code parent}. + private static boolean isDescendant(View parent, View child) { + View current = child; + while (current != null) { + if (current == parent) { + return true; + } + if (!(current.getParent() instanceof View)) { + return false; + } + current = (View) current.getParent(); + } + return false; + } + + /** + * Sets the tap target for the given view to encompass at least the area of a square of the given + * dimensions. + * + * <p>If the current tap area is already larger in either dimension, method will not shrink it + * (hence "min" tap target). + * + * <p>If the current tap area is smaller, method will expand it equally on either side to meet the + * minimum size. + * + * <p><b>Important: This method works by adding a {@link TouchDelegate} to the container view.</b> + * The caller must make sure this method is invoked only once per view. Otherwise, multiple {@link + * TouchDelegate} instances will be added to the container, which could cause duplicate click + * events. + * + * @param containerView the view where a {@link TouchDelegate} will be added. + * @param view the view to potentially expand the tap target for. + * @param tapTargetSize the dimensions of a square that will become the new minimum tap target for + * the given view. + */ + public static void setMinTapTarget(ViewGroup containerView, View view, int tapTargetSize) { + containerView.post( + () -> { + // Return if the view has already been removed from the view hierarchy or has unexpected + // parent. + if (!(view.getParent() instanceof View) + || !isDescendant(containerView, (View) view.getParent())) { + L.d(LogTags.TEMPLATE, "Cannot set min tap target for view %s", view); + return; + } + + Rect rect = new Rect(); + view.getHitRect(rect); + containerView.offsetDescendantRectToMyCoords((View) view.getParent(), rect); + + int rectHeight = rect.height(); + if (rectHeight < tapTargetSize) { + int delta = (tapTargetSize - rectHeight) / 2; + rect.top -= delta; + rect.bottom += delta; + } + + int rectWidth = rect.width(); + if (rectWidth < tapTargetSize) { + int delta = (tapTargetSize - rectWidth) / 2; + rect.left -= delta; + rect.right += delta; + } + + TouchDelegate parentTouchDelegate = containerView.getTouchDelegate(); + TouchDelegate newDelegate = new TouchDelegate(rect, view); + if (parentTouchDelegate != null) { + if (parentTouchDelegate instanceof TouchDelegateComposite) { + ((TouchDelegateComposite) parentTouchDelegate).addDelegate(newDelegate, view); + newDelegate = parentTouchDelegate; + } else { + TouchDelegateComposite composite = new TouchDelegateComposite(view); + composite.addDelegate(parentTouchDelegate, null); + composite.addDelegate(newDelegate, view); + newDelegate = composite; + } + } + containerView.setTouchDelegate(newDelegate); + }); + } + + /** + * Enforce the minimum and maximum size limit to the given view. + * + * <p>The view width and height sizes must be equal. + */ + public static void enforceViewSizeLimit(View view, int minSize, int maxSize) { + enforceViewSizeLimit( + view, + /* minWidth= */ minSize, + /* maxWidth= */ maxSize, + /* minHeight= */ minSize, + /* maxHeight= */ maxSize); + } + + /** Enforce the minimum and maximum width and height limits to the given view. */ + public static void enforceViewSizeLimit( + View view, int minWidth, int maxWidth, int minHeight, int maxHeight) { + LayoutParams layoutParams = view.getLayoutParams(); + if (layoutParams == null) { + return; + } + + int width = getValueInRange(layoutParams.width, minWidth, maxWidth); + int height = getValueInRange(layoutParams.height, minHeight, maxHeight); + layoutParams.width = width; + layoutParams.height = height; + view.setLayoutParams(layoutParams); + } + + /** Logs a telemetry event with the given {@link UiAction} and {@link TemplateContext} */ + public static void logCarAppTelemetry(TemplateContext templateContext, UiAction action) { + logCarAppTelemetry( + templateContext, + TelemetryEvent.newBuilder(action) + .setComponentName(templateContext.getCarAppPackageInfo().getComponentName())); + } + + /** + * Logs a telemetry event with the given {@link UiAction}, action count and {@link + * TemplateContext} + */ + public static void logCarAppTelemetry( + TemplateContext templateContext, UiAction action, int actionCount) { + logCarAppTelemetry( + templateContext, + TelemetryEvent.newBuilder(action) + .setComponentName(templateContext.getCarAppPackageInfo().getComponentName()) + .setItemsLoadedCount(actionCount)); + } + + /** + * Logs a telemetry event with the given {@link TelemetryEvent.Builder} and {@link + * TemplateContext} + */ + public static void logCarAppTelemetry( + TemplateContext templateContext, TelemetryEvent.Builder builder) { + TelemetryHandler telemetry = templateContext.getTelemetryHandler(); + telemetry.logCarAppTelemetry(builder); + } + + /** + * Returns the capped value between the min and max range. + * + * <p>If the given value is less than or equal to 0 (e.g. MATCH_CONSTRAINT (0), MATCH_PARENT (-1), + * or WRAP_CONTENT (-2)), the original value will be returned. + */ + private static int getValueInRange(int value, int min, int max) { + if (value <= 0) { + return value; + } + + int newValue = value; + newValue = min(newValue, max); + newValue = max(newValue, min); + return newValue; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/anim/fab_view_animation_fade_in.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/anim/fab_view_animation_fade_in.xml new file mode 100644 index 0000000..5a8ca7b --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/anim/fab_view_animation_fade_in.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> +<set xmlns:android="http://schemas.android.com/apk/res/android" + android:ordering="together"> + <objectAnimator + android:duration="@integer/action_strip_animation_duration_millis" + android:propertyName="scaleX" + android:valueType="floatType" + android:valueFrom=".7f" + android:valueTo="1f"/> + <objectAnimator + android:duration="@integer/action_strip_animation_duration_millis" + android:propertyName="scaleY" + android:valueType="floatType" + android:valueFrom=".7f" + android:valueTo="1f"/> + <objectAnimator + android:duration="@integer/action_strip_animation_duration_millis" + android:propertyName="alpha" + android:valueType="floatType" + android:valueFrom="0f" + android:valueTo="1f"/> +</set> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/anim/fab_view_animation_fade_out.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/anim/fab_view_animation_fade_out.xml new file mode 100644 index 0000000..33f5c80 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/anim/fab_view_animation_fade_out.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> +<set xmlns:android="http://schemas.android.com/apk/res/android" + android:ordering="together"> + <objectAnimator + android:duration="@integer/action_strip_animation_duration_millis" + android:propertyName="scaleX" + android:valueType="floatType" + android:valueFrom="1f" + android:valueTo=".7f"/> + <objectAnimator + android:duration="@integer/action_strip_animation_duration_millis" + android:propertyName="scaleY" + android:valueType="floatType" + android:valueFrom="1f" + android:valueTo=".7f"/> + <objectAnimator + android:duration="@integer/action_strip_animation_duration_millis" + android:propertyName="alpha" + android:valueType="floatType" + android:valueFrom="1f" + android:valueTo="0f"/> +</set> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/anchor_marker.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/anchor_marker.xml new file mode 100644 index 0000000..debd0a3 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/anchor_marker.xml @@ -0,0 +1,27 @@ +<!-- + Copyright (C) 2021 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 + + https://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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="50dp" + android:viewportWidth="36" + android:viewportHeight="50"> + <path + android:pathData="M28.2,32c5.2,-4.9 6.8,-8.3 6.8,-14c0,-9.4 -7.6,-17 -17,-17S1,8.6 1,18c0,5.6 1.6,9 6.7,14l1.2,1c5,4.9 6.7,8.2 7.3,14.1c0.1,1.1 0.8,1.8 1.8,1.8c1,0 1.7,-0.7 1.8,-1.8c0.5,-5.8 2.2,-9.1 7.2,-14L28.2,32z" + android:strokeWidth="0" + android:fillColor="#FFFFFF" + android:fillType="nonZero" + android:strokeColor="#00000000"/> +</vector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/anchor_marker_border.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/anchor_marker_border.xml new file mode 100644 index 0000000..063b6eb --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/anchor_marker_border.xml @@ -0,0 +1,27 @@ +<!-- + Copyright (C) 2021 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 + + https://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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="50dp" + android:viewportWidth="36" + android:viewportHeight="50"> + <path + android:pathData="M18,49.9C16.5,49.9 15.3,48.8 15.2,47.2C14.7,41.6 13.1,38.5 8.3,33.8L7,32.7C1.7,27.5 0,23.9 0,18C0,8.1 8.1,0 18,0C27.9,0 36,8.1 36,18C36,24 34.3,27.6 28.9,32.7L28.6,33L27.7,33.8C22.9,38.5 21.3,41.6 20.8,47.2C20.6,48.8 19.5,49.9 18,49.9ZM8.3,31.2L8.7,31.5L9.7,32.4C14.9,37.5 16.7,41 17.2,47.1C17.2,47.6 17.5,48 18,48C18.6,48 18.8,47.4 18.8,47.1C19.3,41.1 21.2,37.5 26.3,32.5L27.5,31.4C32.5,26.6 34,23.6 34,18.1C34,9.3 26.8,2.1 18,2.1C9.2,2.1 2,9.2 2,18C2,23.3 3.5,26.4 8.3,31.2Z" + android:strokeWidth="1" + android:fillColor="#80868B" + android:fillType="nonZero" + android:strokeColor="#00000000"/> +</vector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/anchor_marker_circle.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/anchor_marker_circle.xml new file mode 100644 index 0000000..9953ce3 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/anchor_marker_circle.xml @@ -0,0 +1,27 @@ +<!-- + Copyright (C) 2021 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 + + https://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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="50dp" + android:viewportWidth="36" + android:viewportHeight="50"> + <path + android:pathData="M18,18m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0" + android:strokeWidth="1" + android:fillColor="#80868B" + android:fillType="nonZero" + android:strokeColor="#00000000"/> +</vector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/empty.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/empty.xml new file mode 100644 index 0000000..89d860b --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/empty.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!-- This is a blank shape, 0x0 in size, that works around the fact that the + android:textSelectHandle xml property requires a drawable with a defined size. --> +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle" > + <size + android:width="0dp" + android:height="0dp" /> +</shape> + diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/ic_error.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/ic_error.xml new file mode 100644 index 0000000..9393c31 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/ic_error.xml @@ -0,0 +1,19 @@ +<!-- + Copyright (C) 2021 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 + + https://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. +--> +<vector android:height="64dp" android:viewportHeight="24" + android:viewportWidth="24" android:width="64dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#FFFFFF" android:pathData="M12,5.99L19.53,19L4.47,19L12,5.99M12,2L1,21h22L12,2zM13,16h-2v2h2v-2zM13,10h-2v4h2v-4z"/> +</vector> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/ic_pan_overlay.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/ic_pan_overlay.xml new file mode 100644 index 0000000..79933ad --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/ic_pan_overlay.xml @@ -0,0 +1,34 @@ +<!-- + Copyright (C) 2021 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 + + https://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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="290dp" + android:height="258dp" + android:viewportWidth="290" + android:viewportHeight="258"> + <path + android:pathData="M244.268,130.768C245.244,129.791 245.244,128.209 244.268,127.232L228.358,111.322C227.382,110.346 225.799,110.346 224.822,111.322C223.846,112.299 223.846,113.881 224.822,114.858L238.964,129L224.822,143.142C223.846,144.118 223.846,145.701 224.822,146.678C225.799,147.654 227.381,147.654 228.358,146.678L244.268,130.768ZM241,131.5L242.5,131.5L242.5,126.5L241,126.5L241,131.5Z" + android:fillColor="#000000"/> + <path + android:pathData="M54.232,127.232C53.256,128.208 53.256,129.791 54.232,130.768L70.142,146.678C71.118,147.654 72.701,147.654 73.677,146.678C74.654,145.702 74.654,144.119 73.677,143.143L59.535,129L73.678,114.858C74.654,113.882 74.655,112.299 73.678,111.323C72.702,110.347 71.119,110.346 70.143,111.323L54.232,127.232ZM57.5,126.5L56,126.5L56,131.5L57.5,131.5L57.5,126.5Z" + android:fillColor="#000000"/> + <path + android:pathData="M147.232,224.268C148.209,225.244 149.791,225.244 150.768,224.268L166.678,208.358C167.654,207.381 167.654,205.799 166.678,204.822C165.701,203.846 164.118,203.846 163.142,204.822L149,218.964L134.858,204.822C133.881,203.846 132.299,203.846 131.322,204.822C130.346,205.799 130.346,207.382 131.322,208.358L147.232,224.268ZM146.5,221L146.5,222.5L151.5,222.5L151.5,221L146.5,221Z" + android:fillColor="#000000"/> + <path + android:pathData="M150.768,34.232C149.791,33.256 148.209,33.256 147.232,34.232L131.322,50.142C130.346,51.118 130.346,52.701 131.322,53.678C132.299,54.654 133.882,54.654 134.858,53.678L149,39.535L163.142,53.678C164.118,54.654 165.701,54.654 166.678,53.678C167.654,52.701 167.654,51.118 166.678,50.142L150.768,34.232ZM151.5,39L151.5,36L146.5,36L146.5,39L151.5,39Z" + android:fillColor="#000000"/> +</vector> + diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_list_row.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_list_row.xml new file mode 100644 index 0000000..171f33a --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_list_row.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView + android:id="@+id/action_button_list_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="?templatePlainContentHorizontalPadding" + android:layout_marginVertical="?templateActionButtonListRowVerticalSpacing" + android:orientation="horizontal" + android:gravity="center" /> +</FrameLayout> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_list_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_list_view.xml new file mode 100644 index 0000000..d9fdc29 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_list_view.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:orientation="horizontal" + android:gravity="center" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="?templatePlainContentHorizontalPadding"> +</com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view.xml new file mode 100644 index 0000000..1a8fd6d --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<com.android.car.libraries.templates.host.view.widgets.common.ActionButtonView + xmlns:android="http://schemas.android.com/apk/res/android" + android:clickable="true" + android:focusable="true" + android:layout_width="wrap_content" + android:layout_height="?templateActionButtonHeight" + style="?templateActionButtonStyle"/> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view_icon.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view_icon.xml new file mode 100644 index 0000000..c178a8f --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view_icon.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<com.android.car.libraries.templates.host.view.widgets.common.CarImageView + 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:id="@+id/action_icon" + android:layout_width="?templateActionIconSize" + android:layout_height="?templateActionIconSize" + app:imageMinWidth="?templateActionIconSizeMin" + app:imageMaxWidth="?templateActionIconSizeMax" + app:imageMinHeight="?templateActionIconSizeMin" + app:imageMaxHeight="?templateActionIconSizeMax" + android:layout_gravity="center" + android:scaleType="fitCenter" + tools:ignore="ContentDescription"/> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view_icon_text.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view_icon_text.xml new file mode 100644 index 0000000..7d4bdce --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view_icon_text.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> +<LinearLayout + 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_gravity="center" + android:orientation="horizontal" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingStart="?templateActionIconTextStartSpacing" + android:paddingEnd="?templateActionIconTextEndSpacing"> + + <com.android.car.libraries.templates.host.view.widgets.common.CarImageView + android:id="@+id/action_icon" + android:layout_width="?templateActionIconSize" + android:layout_height="?templateActionIconSize" + app:imageMinWidth="?templateActionIconSizeMin" + app:imageMaxWidth="?templateActionIconSizeMax" + app:imageMinHeight="?templateActionIconSizeMin" + app:imageMaxHeight="?templateActionIconSizeMax" + android:layout_gravity="center" + android:scaleType="fitCenter" + tools:ignore="ContentDescription"/> + + <CarUiTextView + android:id="@+id/action_text" + style="?templateActionButtonTextStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginStart="?templateActionIconToTextSpacing" + android:maxEms="?templateActionButtonTextMaxEmsWithIcon"/> +</LinearLayout> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view_text.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view_text.xml new file mode 100644 index 0000000..c5b82d4 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view_text.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<CarUiTextView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/action_text" + style="?templateActionButtonTextStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginHorizontal="?templateActionTextHorizontalSpacing" + android:layout_gravity="center" + android:maxEms="?templateActionButtonTextMaxEmsNoIcon"/> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_strip_view_floating.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_strip_view_floating.xml new file mode 100644 index 0000000..1a75313 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_strip_view_floating.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!-- +An action strip with floating buttons that anchors to the top right of the +screen, meant to be used in conjunction with half-screen, card-style templates. + +IMPORTANT: parents of this view should have clipChildren set to false so that +the shadows don't get clipped. +--> +<com.android.car.libraries.templates.host.view.widgets.common.ActionStripView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:focusable="false" + android:visibility="gone" + android:clipChildren="false" + app:fabAppearance="?templateActionStripFabAppearance"> + + <LinearLayout + android:id="@+id/action_strip_touch_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="?templateActionStripPadding" + android:orientation="vertical"> + <LinearLayout + android:id="@+id/action_strip_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:clipChildren="false" + android:layout_gravity="center_vertical|end" + android:orientation="horizontal" /> + + <LinearLayout + android:id="@+id/action_strip_container_secondary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:clipChildren="false" + android:layout_gravity="center_vertical|end" + android:orientation="horizontal" /> + </LinearLayout> + +</com.android.car.libraries.templates.host.view.widgets.common.ActionStripView> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/card_container.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/card_container.xml new file mode 100644 index 0000000..848f4ba --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/card_container.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!-- The card has a minimum and maximum heights, the latter never going past + the screen height. --> +<com.android.car.libraries.templates.host.view.widgets.common.BleedingCardView + 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" + style="?templateCardContentContainerStyle" + android:visibility="gone" + android:focusable="false" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintVertical_bias="0.0" + app:layout_constraintHeight_default="wrap" + app:layout_constraintHeight_min="?templateCardContentContainerMinHeight" + android:layout_marginStart="?templateCardContentContainerStartMargin" + android:layout_marginTop="?templateCardContentContainerTopMargin" + android:layout_width="?templateCardContentContainerDefaultWidth" + android:layout_height="0dp" + tools:ignore="MissingClass"> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + <include layout="@layout/card_header_layout"/> + <View + android:layout_width="match_parent" + android:layout_height="?templateDividerThickness" + android:background="?templateDividerColor"/> + <include + layout="@layout/content_view" + android:id="@+id/content_view"/> + </LinearLayout> +</com.android.car.libraries.templates.host.view.widgets.common.BleedingCardView> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/card_header_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/card_header_layout.xml new file mode 100644 index 0000000..6540a2c --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/card_header_layout.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:focusable="false" + tools:ignore="MergeRootFrame"> + <com.android.car.libraries.templates.host.view.widgets.common.CardHeaderView + android:id="@+id/header_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:focusable="false" + android:descendantFocusability="afterDescendants"/> +</FrameLayout> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/clickable_span_text_container.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/clickable_span_text_container.xml new file mode 100644 index 0000000..2aad758 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/clickable_span_text_container.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> +<com.android.car.libraries.templates.host.view.widgets.common.ClickableSpanTextContainer + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:focusable="false" + android:descendantFocusability="afterDescendants" > + + <CarUiTextView + android:id="@+id/clickable_span_text_view" + style="?templateSignInAdditionalTextStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:focusable="true" /> + +</com.android.car.libraries.templates.host.view.widgets.common.ClickableSpanTextContainer> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/content_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/content_view.xml new file mode 100644 index 0000000..2399b86 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/content_view.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<com.android.car.libraries.templates.host.view.widgets.common.ContentView + xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_height="match_parent" + android:layout_width="match_parent"> + + <!-- A container for the content views's content. --> + <com.android.car.ui.FocusArea + android:id="@+id/container" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_gravity="center" + android:gravity="center" + android:orientation="vertical" + android:clipChildren="true" + android:layout_weight="1"/> + +</com.android.car.libraries.templates.host.view.widgets.common.ContentView> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/driving_message_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/driving_message_view.xml new file mode 100644 index 0000000..090ebf5 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/driving_message_view.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> +<LinearLayout + 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:id="@+id/driving_message_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:gravity="center" + android:orientation="vertical" + android:background="@color/template_black" + android:visibility="gone"> + <!-- An icon shown on top of the contents. --> + <com.android.car.libraries.templates.host.view.widgets.common.CarImageView + android:id="@+id/driving_message_icon" + android:layout_width="?templateLargeImageSize" + android:layout_height="?templateLargeImageSize" + app:imageMinWidth="?templateLargeImageSizeMin" + app:imageMaxWidth="?templateLargeImageSizeMax" + app:imageMinHeight="?templateLargeImageSizeMin" + app:imageMaxHeight="?templateLargeImageSizeMax" + android:src="@drawable/ic_error" + tools:ignore="ContentDescription" /> + + <!-- The title displayed below the icon. --> + <CarUiTextView + android:id="@+id/driving_message_text" + style="?templateMessageTitleTextStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginTop="?templateMessageTitleTopSpacing" + android:foreground="@drawable/no_content_view_focus_ring" /> +</LinearLayout> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/fab_view_icon_text.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/fab_view_icon_text.xml new file mode 100644 index 0000000..c638c3f --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/fab_view_icon_text.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> +<LinearLayout + 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_gravity="center" + android:orientation="horizontal" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="?templateActionIconTextStartSpacing" + android:layout_marginEnd="?templateActionIconTextEndSpacing"> + + <com.android.car.libraries.templates.host.view.widgets.common.CarImageView + android:id="@+id/action_icon" + android:layout_width="?templateActionIconSize" + android:layout_height="?templateActionIconSize" + app:imageMinWidth="?templateActionIconSizeMin" + app:imageMaxWidth="?templateActionIconSizeMax" + app:imageMinHeight="?templateActionIconSizeMin" + app:imageMaxHeight="?templateActionIconSizeMax" + android:layout_gravity="center" + android:scaleType="fitCenter" + tools:ignore="ContentDescription"/> + + <CarUiTextView + android:id="@+id/action_text" + style="?templateActionButtonTextStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginStart="?templateActionIconToTextSpacing" + android:maxEms="?templateFabTextMaxEmsWithIcon"/> +</LinearLayout> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/fab_view_text.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/fab_view_text.xml new file mode 100644 index 0000000..3bc77c8 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/fab_view_text.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> +<CarUiTextView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/action_text" + style="?templateActionButtonTextStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginHorizontal="?templateActionTextHorizontalSpacing" + android:layout_gravity="center" + android:maxEms="?templateFabTextMaxEmsNoIcon"/> + diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/fab_view_text_no_icon.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/fab_view_text_no_icon.xml new file mode 100644 index 0000000..826c98f --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/fab_view_text_no_icon.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<CarUiTextView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/action_text" + style="?templateActionButtonTextStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:paddingStart="@dimen/template_padding_5" + android:paddingEnd="@dimen/template_padding_5" + android:maxEms="?templateFabTextMaxEmsNoIcon"/> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_list_no_divider_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_list_no_divider_view.xml new file mode 100644 index 0000000..6855f5c --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_list_no_divider_view.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<com.android.car.libraries.templates.host.view.widgets.common.RowListView + 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:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:markerAppearance="?templateListMarkerAppearance"> + + <FrameLayout + android:id="@+id/progress_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:minHeight="?templateRowMinHeight" + android:focusable="true" + android:visibility="gone"> + <com.android.car.libraries.templates.host.view.widgets.common.CarProgressBar + android:id="@+id/progress" + style="?templateLoadingSpinnerStyle" + android:layout_width="?templateLargeImageSize" + android:layout_height="?templateLargeImageSize" + app:imageMinSize="?templateLargeImageSizeMin" + app:imageMaxSize="?templateLargeImageSizeMax" + android:layout_gravity="center" /> + </FrameLayout> + + <!-- A text view displaying a message when the list is empty. --> + <CarUiTextView + style="?templateRowListEmptyTextStyle" + android:id="@+id/list_no_items_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:foreground="@drawable/no_content_view_focus_ring" /> + + <com.android.car.ui.recyclerview.CarUiRecyclerView + android:id="@+id/list_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clickable="true" + android:clipToPadding="true" + app:carUiSize="large" + app:layoutStyle="linear" + app:enableDivider="false" /> + + <FrameLayout + android:id="@+id/large_image_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="top|end" + android:clickable="false" + android:focusable="false" + android:visibility="gone"> + <!-- + We programmatically calculate the width and height of the wrapping + large_image_container, and allow the ImageView to stretch to fill the + container width if needed while maintaining the source's aspect ratio. + --> + <com.android.car.libraries.templates.host.view.widgets.common.CarImageView + android:id="@+id/large_image" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="top|center" + android:clickable="false" + android:focusable="false" + android:scaleType="fitCenter" + android:adjustViewBounds="true" + tools:ignore="ContentDescription" /> + </FrameLayout> + +</com.android.car.libraries.templates.host.view.widgets.common.RowListView> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_list_row_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_list_row_view.xml new file mode 100644 index 0000000..581b88d --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_list_row_view.xml @@ -0,0 +1,202 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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" + android:layout_width="match_parent" + android:layout_height="wrap_content" + xmlns:tools="http://schemas.android.com/tools" + android:tag="carUiListItem"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="0dp" + app:layout_constraintEnd_toStartOf="@id/large_image_spacer" + app:layout_constraintStart_toStartOf="parent" + android:layout_height="wrap_content" + android:minHeight="@dimen/car_ui_list_item_height"> + + <!-- The following touch interceptor views are sized to encompass the specific sub-sections of + the list item view to easily control the bounds of a background ripple effects. --> + <View + android:id="@+id/car_ui_list_item_touch_interceptor" + android:layout_width="0dp" + android:layout_height="0dp" + android:background="@drawable/car_ui_list_item_background" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <!-- This touch interceptor does not include the action container --> + <View + android:id="@+id/car_ui_list_item_reduced_touch_interceptor" + android:layout_width="0dp" + android:layout_height="0dp" + android:background="@drawable/car_ui_list_item_background" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/car_ui_list_item_action_container" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/car_ui_list_item_start_guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_begin="?templateFullRowStartPadding" /> + + <FrameLayout + android:id="@+id/car_ui_list_item_icon_container" + android:layout_width="@dimen/car_ui_list_item_icon_container_width" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="@+id/car_ui_list_item_start_guideline" + app:layout_constraintTop_toTopOf="parent"> + + <ImageView + android:id="@+id/car_ui_list_item_icon" + android:layout_width="@dimen/car_ui_list_item_icon_size" + android:layout_height="@dimen/car_ui_list_item_icon_size" + android:layout_gravity="center" + android:visibility="gone" + android:scaleType="fitCenter" + tools:ignore="ContentDescription"/> + + <ImageView + android:id="@+id/car_ui_list_item_content_icon" + android:layout_width="@dimen/car_ui_list_item_content_icon_width" + android:layout_height="@dimen/car_ui_list_item_content_icon_height" + android:layout_gravity="center" + android:visibility="gone" + android:scaleType="fitCenter" + tools:ignore="ContentDescription"/> + + <ImageView + android:id="@+id/car_ui_list_item_avatar_icon" + android:background="@drawable/car_ui_list_item_avatar_icon_outline" + android:layout_width="@dimen/car_ui_list_item_avatar_icon_width" + android:layout_height="@dimen/car_ui_list_item_avatar_icon_height" + android:layout_gravity="center" + android:visibility="gone" + android:scaleType="fitCenter" + tools:ignore="ContentDescription"/> + </FrameLayout> + + <CarUiTextView + android:id="@+id/car_ui_list_item_title" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/car_ui_list_item_text_start_margin" + style="?templateRowTitleStyle" + android:layout_marginTop="@dimen/car_ui_padding_2" + app:layout_constraintBottom_toTopOf="@+id/car_ui_list_item_body" + app:layout_constraintEnd_toStartOf="@+id/car_ui_list_item_action_container" + app:layout_constraintStart_toEndOf="@+id/car_ui_list_item_icon_container" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_chainStyle="packed" + app:layout_goneMarginBottom="@dimen/car_ui_padding_2" + app:layout_goneMarginStart="0dp"/> + <CarUiTextView + android:id="@+id/car_ui_list_item_body" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/car_ui_list_item_text_start_margin" + style="?templateRowSecondaryTextStyle" + android:layout_marginBottom="@dimen/car_ui_padding_2" + android:textAlignment="viewStart" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/car_ui_list_item_action_container" + app:layout_constraintStart_toEndOf="@+id/car_ui_list_item_icon_container" + app:layout_constraintTop_toBottomOf="@+id/car_ui_list_item_title" + app:layout_goneMarginTop="@dimen/car_ui_padding_2" + app:layout_goneMarginStart="0dp"/> + + <!-- This touch interceptor is sized and positioned to encompass the action container --> + <View + android:id="@+id/car_ui_list_item_action_container_touch_interceptor" + android:layout_width="0dp" + android:layout_height="0dp" + android:background="@drawable/car_ui_list_item_background" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="@id/car_ui_list_item_action_container" + app:layout_constraintEnd_toEndOf="@id/car_ui_list_item_action_container" + app:layout_constraintStart_toStartOf="@id/car_ui_list_item_action_container" + app:layout_constraintTop_toTopOf="@id/car_ui_list_item_action_container" /> + + <FrameLayout + android:id="@+id/car_ui_list_item_action_container" + android:layout_width="wrap_content" + android:minWidth="@dimen/car_ui_list_item_icon_container_width" + android:layout_height="0dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="@+id/car_ui_list_item_end_guideline" + app:layout_constraintTop_toTopOf="parent"> + + <Switch + android:id="@+id/car_ui_list_item_switch_widget" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:clickable="false" + android:focusable="false" /> + + <CheckBox + android:id="@+id/car_ui_list_item_checkbox_widget" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:clickable="false" + android:focusable="false" /> + + <RadioButton + android:id="@+id/car_ui_list_item_radio_button_widget" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:clickable="false" + android:focusable="false" /> + + <ImageView + android:id="@+id/car_ui_list_item_supplemental_icon" + android:layout_width="?templateFullRowChevronWidth" + android:layout_height="?templateFullRowChevronHeight" + android:layout_gravity="center" + android:scaleType="fitCenter" + tools:ignore="ContentDescription"/> + </FrameLayout> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/car_ui_list_item_end_guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_end="?templateFullRowEndPadding" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <Space + android:id="@+id/large_image_spacer" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintWidth_percent="?templateRowListToLargeImageRatio" + app:layout_constraintWidth_default="percent" + app:layout_constraintWidth_max="?templateRowListLargeImageContainerMaxWidth" + app:layout_constraintEnd_toEndOf="parent" + android:visibility="gone" /> + +</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_list_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_list_view.xml new file mode 100644 index 0000000..2cadd94 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_list_view.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<com.android.car.libraries.templates.host.view.widgets.common.RowListView + 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:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:markerAppearance="?templateListMarkerAppearance"> + + <FrameLayout + android:id="@+id/progress_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:minHeight="?templateRowMinHeight" + android:focusable="true" + android:visibility="gone"> + <com.android.car.libraries.templates.host.view.widgets.common.CarProgressBar + android:id="@+id/progress" + style="?templateLoadingSpinnerStyle" + android:layout_width="?templateLargeImageSize" + android:layout_height="?templateLargeImageSize" + app:imageMinSize="?templateLargeImageSizeMin" + app:imageMaxSize="?templateLargeImageSizeMax" + android:layout_gravity="center" /> + </FrameLayout> + + <!-- A text view displaying a message when the list is empty. --> + <CarUiTextView + style="?templateRowListEmptyTextStyle" + android:id="@+id/list_no_items_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:foreground="@drawable/no_content_view_focus_ring" /> + + <com.android.car.ui.recyclerview.CarUiRecyclerView + android:id="@+id/list_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clickable="true" + android:clipToPadding="true" + app:carUiSize="large" + app:layoutStyle="linear" + app:enableDivider="true" /> + + <FrameLayout + android:id="@+id/large_image_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="top|end" + android:clickable="false" + android:focusable="false" + android:visibility="gone"> + <!-- + We programmatically calculate the width and height of the wrapping + large_image_container, and allow the ImageView to stretch to fill the + container width if needed while maintaining the source's aspect ratio. + --> + <com.android.car.libraries.templates.host.view.widgets.common.CarImageView + android:id="@+id/large_image" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="top|center" + android:clickable="false" + android:focusable="false" + android:scaleType="fitCenter" + android:adjustViewBounds="true" + tools:ignore="ContentDescription" /> + </FrameLayout> + +</com.android.car.libraries.templates.host.view.widgets.common.RowListView> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_screen_header_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_screen_header_layout.xml new file mode 100644 index 0000000..44a8ef3 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_screen_header_layout.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="?templateHeaderHeight" + android:paddingStart="@dimen/template_edge_column_margin" + android:paddingEnd="@dimen/template_edge_column_margin" + android:focusable="false" + tools:ignore="MergeRootFrame"> + <com.android.car.libraries.templates.host.view.widgets.common.CardHeaderView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/header_view" + android:layout_width="match_parent" + android:layout_height="?templateHeaderHeight" + android:focusable="false" + android:descendantFocusability="afterDescendants"/> +</FrameLayout> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/grid_item_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/grid_item_view.xml new file mode 100644 index 0000000..7700102 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/grid_item_view.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<com.android.car.libraries.templates.host.view.widgets.common.GridItemView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingVertical="?templateGridItemVerticalSpacing" + android:paddingHorizontal="?templateGridItemHorizontalSpacing" + android:focusable="true" + android:gravity="center" + android:orientation="vertical"> + + <LinearLayout + android:id="@+id/grid_item_image_container" + android:layout_gravity="center_horizontal" + android:orientation="horizontal" + android:gravity="center" + android:layout_width="?templateLargeImageSize" + android:layout_height="?templateLargeImageSize" + android:layout_marginBottom="?templateGridItemImageBottomPadding"> + <!-- The loading spinner. --> + <ProgressBar + android:id="@+id/grid_item_progress_bar" + style="?templateLoadingSpinnerStyle" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" /> + + <!-- The grid item image or icon. --> + <ImageView + android:id="@+id/grid_item_image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:scaleType="fitCenter" + tools:ignore="ContentDescription" /> + </LinearLayout> + + <!-- A container with the title and a secondary text line. --> + <LinearLayout + android:id="@+id/grid_item_text_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:orientation="vertical"> + + <CarUiTextView + android:id="@+id/grid_item_title" + style="?templateGridItemTitleStyle" + android:layout_marginBottom="?templateGridItemTextBottomPadding" + android:visibility="gone" + android:maxWidth="?templateGridItemTextContainerMaxWidth" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal"/> + + <CarUiTextView + android:id="@+id/grid_item_text" + style="?templateGridItemTextStyle" + android:visibility="gone" + android:maxWidth="?templateGridItemTextContainerMaxWidth" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal"/> + </LinearLayout> +</com.android.car.libraries.templates.host.view.widgets.common.GridItemView> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/grid_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/grid_view.xml new file mode 100644 index 0000000..f9ba1f5 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/grid_view.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<com.android.car.libraries.templates.host.view.widgets.common.GridView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:clipChildren="true" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <FrameLayout + android:id="@+id/progress_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:focusable="true" + android:visibility="gone"> + <com.android.car.libraries.templates.host.view.widgets.common.CarProgressBar + android:id="@+id/progress" + style="?templateLoadingSpinnerStyle" + android:layout_width="?templateLargeImageSize" + android:layout_height="?templateLargeImageSize" + app:imageMinSize="?templateLargeImageSizeMin" + app:imageMaxSize="?templateLargeImageSizeMax" + android:layout_gravity="center" /> + </FrameLayout> + + <!-- A text view displaying a message when the list is empty. --> + <CarUiTextView + style="?templateGridEmptyTextStyle" + android:id="@+id/list_no_items_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:foreground="@drawable/no_content_view_focus_ring" /> + + <com.android.car.ui.recyclerview.CarUiRecyclerView + android:id="@+id/grid_paged_list_view" + style="?templateGridStyle" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingHorizontal="?templatePlainContentHorizontalPadding" /> +</com.android.car.libraries.templates.host.view.widgets.common.GridView> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/half_list_row_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/half_list_row_view.xml new file mode 100644 index 0000000..e15c9e6 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/half_list_row_view.xml @@ -0,0 +1,188 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?templateHalfRowMinHeight" + android:tag="carUiListItem"> + + <!-- The following touch interceptor views are sized to encompass the specific sub-sections of + the list item view to easily control the bounds of a background ripple effects. --> + <View + android:id="@+id/car_ui_list_item_touch_interceptor" + android:layout_width="0dp" + android:layout_height="0dp" + android:background="@drawable/car_ui_list_item_background" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <!-- This touch interceptor does not include the action container --> + <View + android:id="@+id/car_ui_list_item_reduced_touch_interceptor" + android:layout_width="0dp" + android:layout_height="0dp" + android:background="@drawable/car_ui_list_item_background" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/car_ui_list_item_action_container" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/car_ui_list_item_start_guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_begin="?templateHalfRowHorizontalPadding" /> + + <FrameLayout + android:id="@+id/car_ui_list_item_icon_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintStart_toStartOf="@+id/car_ui_list_item_start_guideline" + app:layout_constraintTop_toTopOf="@+id/car_ui_list_item_title" > + + <ImageView + android:id="@+id/car_ui_list_item_icon" + android:layout_width="?templateHalfRowImageSize" + android:layout_height="?templateHalfRowImageSize" + android:layout_gravity="center" + android:visibility="gone" + android:scaleType="fitCenter" /> + + <ImageView + android:id="@+id/car_ui_list_item_content_icon" + android:layout_width="?templateHalfRowImageSize" + android:layout_height="?templateHalfRowImageSize" + android:layout_gravity="center" + android:visibility="gone" + android:scaleType="fitCenter" /> + + <ImageView + android:id="@+id/car_ui_list_item_avatar_icon" + android:background="@drawable/car_ui_list_item_avatar_icon_outline" + android:layout_width="?templateHalfRowImageSize" + android:layout_height="?templateHalfRowImageSize" + android:layout_gravity="center" + android:visibility="gone" + android:scaleType="fitCenter" /> + </FrameLayout> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/car_ui_list_item_top_guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_begin="?templateHalfRowVerticalPadding" /> + + <CarUiTextView + android:id="@+id/car_ui_list_item_title" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="?templateHalfRowImageToTextSpacing" + style="?templateRowTitleStyle" + app:layout_constraintBottom_toTopOf="@+id/car_ui_list_item_body" + app:layout_constraintEnd_toStartOf="@+id/car_ui_list_item_action_container" + app:layout_constraintStart_toEndOf="@+id/car_ui_list_item_icon_container" + app:layout_constraintTop_toTopOf="@+id/car_ui_list_item_top_guideline" + app:layout_constraintVertical_chainStyle="packed" + app:layout_goneMarginStart="@dimen/car_ui_padding_0" /> + <CarUiTextView + android:id="@+id/car_ui_list_item_body" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="?templateHalfRowImageToTextSpacing" + android:layout_marginTop="?templateHalfRowTextToTextSpacing" + style="?templateRowSecondaryTextStyle" + app:layout_constraintBottom_toBottomOf="@+id/car_ui_list_item_bottom_guideline" + app:layout_constraintEnd_toStartOf="@+id/car_ui_list_item_action_container" + app:layout_constraintStart_toEndOf="@+id/car_ui_list_item_icon_container" + app:layout_constraintTop_toBottomOf="@+id/car_ui_list_item_title" + app:layout_goneMarginStart="@dimen/car_ui_padding_0" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/car_ui_list_item_bottom_guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_end="?templateHalfRowVerticalPadding" /> + + <!-- This touch interceptor is sized and positioned to encompass the action container --> + <View + android:id="@+id/car_ui_list_item_action_container_touch_interceptor" + android:layout_width="0dp" + android:layout_height="0dp" + android:background="@drawable/car_ui_list_item_background" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="@id/car_ui_list_item_action_container" + app:layout_constraintEnd_toEndOf="@id/car_ui_list_item_action_container" + app:layout_constraintStart_toStartOf="@id/car_ui_list_item_action_container" + app:layout_constraintTop_toTopOf="@id/car_ui_list_item_action_container" /> + + <FrameLayout + android:id="@+id/car_ui_list_item_action_container" + android:layout_width="wrap_content" + android:layout_height="0dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="@+id/car_ui_list_item_end_guideline" + app:layout_constraintTop_toTopOf="parent"> + + <Switch + android:id="@+id/car_ui_list_item_switch_widget" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:clickable="false" + android:focusable="false" /> + + <CheckBox + android:id="@+id/car_ui_list_item_checkbox_widget" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:clickable="false" + android:focusable="false" /> + + <RadioButton + android:id="@+id/car_ui_list_item_radio_button_widget" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:clickable="false" + android:focusable="false" /> + + <ImageView + android:id="@+id/car_ui_list_item_supplemental_icon" + android:layout_width="?templateHalfRowImageSize" + android:layout_height="?templateHalfRowImageSize" + android:layout_gravity="center" + android:scaleType="fitCenter" /> + </FrameLayout> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/car_ui_list_item_end_guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_end="?templateHalfRowHorizontalPadding" /> + +</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/half_list_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/half_list_view.xml new file mode 100644 index 0000000..4618e1e --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/half_list_view.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<com.android.car.libraries.templates.host.view.widgets.common.RowListView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:orientation="vertical" + android:clipChildren="true" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:markerAppearance="?templateListMarkerAppearance" + app:listUseCompactRowLayout="true"> + + <FrameLayout + android:id="@+id/progress_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:minHeight="?templateRowMinHeight" + android:focusable="true" + android:visibility="gone"> + <com.android.car.libraries.templates.host.view.widgets.common.CarProgressBar + android:id="@+id/progress" + style="?templateLoadingSpinnerStyle" + android:layout_width="?templateNavCardLargeImageSize" + android:layout_height="?templateNavCardLargeImageSize" + app:imageMinSize="?templateNavCardLargeImageSizeMin" + app:imageMaxSize="?templateNavCardLargeImageSizeMax" + android:layout_gravity="center" /> + </FrameLayout> + + <!-- A text view displaying a message when the list is empty. --> + <CarUiTextView + style="?templateRowListEmptyTextStyle" + android:id="@+id/list_no_items_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginTop="?templateHalfListPaddingVertical" + android:layout_marginBottom="?templateHalfListPaddingVertical" + android:layout_marginHorizontal="?templateHalfRowHorizontalPadding" + android:foreground="@drawable/no_content_view_focus_ring" /> + + <com.android.car.ui.recyclerview.CarUiRecyclerView + android:id="@+id/list_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clickable="true" + android:clipToPadding="true" + android:paddingBottom="?templateHalfListBottomPadding" + app:carUiSize="small" + app:layoutStyle="linear" + app:enableDivider="true" /> + +</com.android.car.libraries.templates.host.view.widgets.common.RowListView> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/header_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/header_view.xml new file mode 100644 index 0000000..3742510 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/header_view.xml @@ -0,0 +1,98 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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"> + + <!-- The header button icon is wrapped by a frame layout to define + the background layer with a focus selector and ripple effects etc. --> + <FrameLayout + android:id="@+id/header_button_container" + android:layout_width="?templateHeaderButtonContainerSize" + android:layout_height="?templateHeaderButtonContainerSize" + android:layout_marginStart="?templateHeaderButtonStartSpacing" + android:addStatesFromChildren="true" + android:background="?templateHeaderButtonBackground" + android:visibility="gone" + android:clickable="true" + android:focusable="true" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" > + + <!-- The header action icon --> + <ImageView + android:id="@+id/header_icon" + android:layout_width="?templateHeaderButtonIconSize" + android:layout_height="?templateHeaderButtonIconSize" + android:layout_gravity="center" + android:scaleType="fitCenter" + android:maxWidth="?templateHeaderButtonContainerSize" + android:maxHeight="?templateHeaderButtonContainerSize" + tools:ignore="ContentDescription"/> + </FrameLayout> + + <!-- The header title --> + <CarUiTextView + android:id="@+id/header_title" + android:layout_height="wrap_content" + android:layout_width="0dp" + android:layout_marginStart="?templateHeaderTextStartSpacing" + android:layout_marginEnd="?templateHeaderTextEndSpacing" + android:layout_marginVertical="?templateHeaderTextVerticalSpacing" + android:maxLines="1" + android:ellipsize="end" + android:textAlignment="textStart" + android:gravity="center_vertical|start" + android:textAppearance="?templateHeaderTextStyle" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toEndOf="@id/header_button_container" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintEnd_toStartOf="@id/refresh_button_container" + app:layout_goneMarginStart="?templateHeaderTextNoIconStartSpacing" /> + + <!-- The optional refresh icon is wrapped by a frame layout to define + the background layer with a focus selector and ripple effects etc. --> + <FrameLayout + android:id="@+id/refresh_button_container" + android:layout_width="?templateHeaderButtonContainerSize" + android:layout_height="?templateHeaderButtonContainerSize" + android:layout_marginStart="?templateHeaderTextEndSpacing" + android:addStatesFromChildren="true" + android:background="?templateHeaderButtonBackground" + android:visibility="gone" + android:clickable="true" + android:focusable="true" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" > + + <!-- The refresh icon --> + <ImageView + android:id="@+id/refresh_icon" + android:layout_width="?templateHeaderButtonIconSize" + android:layout_height="?templateHeaderButtonIconSize" + android:layout_gravity="center" + android:scaleType="fitCenter" + android:maxWidth="?templateHeaderButtonContainerSize" + android:maxHeight="?templateHeaderButtonContainerSize" + tools:ignore="ContentDescription"/> + </FrameLayout> +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/input_sign_in_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/input_sign_in_view.xml new file mode 100644 index 0000000..61b800f --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/input_sign_in_view.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> +<com.android.car.libraries.templates.host.view.widgets.common.InputSignInView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + style="?templateSignInInputViewStyle" + android:orientation="vertical" > + + <!-- + Use a 0x0 drawable for textSelectHandle; null drawable crashes, so does + transparent color. Also set textCursorDrawable to null because this forces + Android to render a cursor using the text color instead of not rendering + one at all. + --> + <com.android.car.libraries.templates.host.view.widgets.common.CarEditText + android:id="@+id/input_sign_in_box" + style="?templateEditTextStyle" + android:inputType="text" + android:imeOptions="actionGo" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:drawablePadding="@dimen/template_padding_1" + android:focusable="true" + android:focusableInTouchMode="false" + android:textCursorDrawable="@null" + android:textSelectHandle="@drawable/empty" + tools:ignore="RtlHardcoded,SpUsage" /> + + <CarUiTextView + android:id="@+id/input_sign_in_error_message" + style="?templateSignInErrorMessageStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" /> + +</com.android.car.libraries.templates.host.view.widgets.common.InputSignInView>
\ No newline at end of file diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/map_action_strip_view_floating.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/map_action_strip_view_floating.xml new file mode 100644 index 0000000..fd77064 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/map_action_strip_view_floating.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<!-- +An action strip with floating buttons that anchors to the top right of the +screen, meant to be used in conjunction with half-screen, card-style templates. + +IMPORTANT: parents of this view should have clipChildren set to false so that +the shadows don't get clipped. +--> +<com.android.car.libraries.templates.host.view.widgets.common.ActionStripView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:focusable="false" + android:visibility="gone" + android:clipChildren="false" + app:fabAppearance="?templateActionStripFabAppearance" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent"> + + <LinearLayout + android:id="@+id/action_strip_touch_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="?templateActionStripPadding" + android:orientation="horizontal"> + <LinearLayout + android:id="@+id/action_strip_container_secondary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:clipChildren="false" + android:layout_marginEnd="?templateActionStripButtonMargin" + android:layout_gravity="center_horizontal|end" + android:orientation="vertical" /> + + <LinearLayout + android:id="@+id/action_strip_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:clipChildren="false" + android:layout_gravity="center_horizontal|end" + android:orientation="vertical" /> + </LinearLayout> +</com.android.car.libraries.templates.host.view.widgets.common.ActionStripView> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/pan_overlay.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/pan_overlay.xml new file mode 100644 index 0000000..31384e8 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/pan_overlay.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> +<com.android.car.libraries.templates.host.view.widgets.common.PanOverlayView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + <ImageView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/ic_pan_overlay" + tools:ignore="ContentDescription" /> + +</com.android.car.libraries.templates.host.view.widgets.common.PanOverlayView> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/pin_sign_in_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/pin_sign_in_view.xml new file mode 100644 index 0000000..663fce3 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/pin_sign_in_view.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> +<com.android.car.libraries.templates.host.view.widgets.common.PinSignInView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?templateSignInPinBackground" + android:padding="?templateSignInPinPadding" + android:gravity="center"> + <CarUiTextView + android:id="@+id/pin_text" + style="?templateSignInPinTextStyle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" /> +</com.android.car.libraries.templates.host.view.widgets.common.PinSignInView> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/qr_code_sign_in_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/qr_code_sign_in_view.xml new file mode 100644 index 0000000..7b9ffb3 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/qr_code_sign_in_view.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> +<com.android.car.libraries.templates.host.view.widgets.common.QRCodeSignInView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + tools:ignore="Overdraw"> + <ImageView + android:id="@+id/qr_code_view" + android:layout_width="?templateSignInQRCodeImageWidth" + android:layout_height="?templateSignInQRCodeImageWidth" + android:layout_gravity="center" + android:scaleType="fitCenter" + android:tint="?android:attr/textColorPrimary" + tools:ignore="ContentDescription" /> +</com.android.car.libraries.templates.host.view.widgets.common.QRCodeSignInView> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/row_section_header_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/row_section_header_view.xml new file mode 100644 index 0000000..462c7fa --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/row_section_header_view.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<CarUiTextView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/row_section_header" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="?templateRowSectionHeaderStyle"/> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/sign_in_button_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/sign_in_button_view.xml new file mode 100644 index 0000000..0f343ae --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/sign_in_button_view.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<com.android.car.libraries.templates.host.view.widgets.common.ActionButtonView + xmlns:android="http://schemas.android.com/apk/res/android" + android:clickable="true" + android:focusable="true" + android:layout_width="wrap_content" + android:layout_height="?templateActionButtonHeight" + style="?templateSignInProviderSignInButtonStyle"> + + <!-- A container for the different optional parts of an action. --> + <LinearLayout + android:id="@+id/action_container" + android:layout_gravity="center" + android:orientation="horizontal" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> +</com.android.car.libraries.templates.host.view.widgets.common.ActionButtonView> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/values/attrs.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/values/attrs.xml new file mode 100644 index 0000000..5c7d401 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/values/attrs.xml @@ -0,0 +1,161 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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> + <declare-styleable name="BleedingCardView"> + <attr name="cardRadius" format="dimension"/> + <attr name="cardBackgroundColor" format="color"/> + <attr name="cardTextColor" format="color"/> + + <!-- The colors used if there is not enough contrast ratio between + cardBackgroundColor and cardTextColor. --> + <attr name="cardFallbackDarkBackgroundColor" format="color" /> + <attr name="cardFallbackLightBackgroundColor" format="color" /> + + <attr name="cardBorderWidth" format="dimension"/> + <attr name="cardBorderColor" format="color" /> + <attr name="cardMinWidth" format="dimension" /> + <attr name="cardMaxWidth" format="dimension" /> + + <!-- The OEM-defined card width. + The value set by the OEM cannot be larger than cardOemMaxWidth. --> + <attr name="cardOemWidth" format="dimension" /> + <attr name="cardOemMaxWidth" format="dimension" /> + + <!-- The card width fraction in comparison to the parent view. + Zero means we use the layout_width, not the fraction. --> + <attr name="cardWidthFraction" format="float" /> + </declare-styleable> + + <declare-styleable name="ListView"> + <!-- The fraction of the screen width that the list will take, up to the max + defined by `listMaxWidth`. A negative value indicates that the list + does ot use an adaptive width. --> + <attr name="listWidthFraction" format="float"/> + + <!-- The maximum width a row list will use regardless of the + screen width. --> + <attr name="listMaxWidth" format="dimension" /> + + <!-- The width of the scrollbar next to the list. --> + <attr name="listScrollbarWidth" format="float"/> + + <!-- The start padding of the list without a scrollbar. --> + <attr name="listNoScrollBarStartPadding" format="dimension"/> + + <!-- Whether to show the scrollbar divider. --> + <attr name="listShowScrollbarDivider" format="boolean"/> + </declare-styleable> + + <declare-styleable name="RowListView"> + <!-- Whether to show the compact row layout. --> + <attr name="listUseCompactRowLayout" format="boolean"/> + </declare-styleable> + + <declare-styleable name="PlaceMarker"> + <!-- Appearance of the map markers. --> + <attr name="markerAppearance" format="reference" /> + </declare-styleable> + + <declare-styleable name="MarkerAppearance"> + <!-- Colors of the POI marker. --> + <attr name="markerDefaultBackgroundColor" format="color" /> + + <!-- Color that should be used for the content if it has a default bg --> + <attr name="markerDefaultContentColor" format="color" /> + + <!-- Color that should be used for the content if it has a custom bg --> + <attr name="markerCustomBackgroundContentColor" format="color" /> + + <!-- Color that should be used for the border if it is a default bg --> + <attr name="markerDefaultBorderColor" format="color" /> + + <!-- Color that should be used for the border if it is a custom bg --> + <attr name="markerCustomBorderColor" format="color" /> + + <attr name="markerPointerWidth" format="dimension" /> + <attr name="markerPointerHeight" format="dimension" /> + <attr name="markerStroke" format="dimension" /> + <attr name="markerCornerRadius" format="dimension" /> + <attr name="markerPadding" format="dimension" /> + + <!-- Colors of the anchor marker. --> + <attr name="anchorDefaultBackgroundColor" format="color" /> + <attr name="anchorBorderColor" format="color" /> + <attr name="anchorDotColor" format="color" /> + + <!-- The following android attributes are used for the marker label style. --> + <attr name="android:textSize" /> + <attr name="android:fontFamily" /> + <attr name="android:textStyle" /> + + <!-- The size of the label within the marker. --> + <attr name="markerTextHorizontalPadding" format="dimension" /> + <attr name="markerIconSize" format="dimension" /> + <attr name="markerImageSize" format="dimension" /> + <attr name="markerImageCornerRadius" format="dimension" /> + + <!-- The size of the marker icon in the list. --> + <attr name="markerListIconSize" format="dimension" /> + </declare-styleable> + + <declare-styleable name="ActionStripView"> + <!-- Appearance of the action strip fabs. --> + <attr name="fabAppearance" format="reference" /> + </declare-styleable> + + <!-- Styleable for different attributes to configure what an action strip FAB should look like. --> + <declare-styleable name="FabAppearance"> + <!-- The color that should be used for contents (icon+label) inside the FAB. --> + <attr name="fabDefaultContentColor" format="color" /> + </declare-styleable> + + <!-- Styleable for configuring the action button --> + <declare-styleable name="ActionButtonView"> + <!-- Specifies the maxEms value for the action button text. Needed to customize the maxEms value for some action buttons --> + <attr name="textMaxEms" format="integer"/> + </declare-styleable> + + <!-- Styleable for configuring the car image view --> + <declare-styleable name="CarImageView"> + <!-- The minimum image width. --> + <attr name="imageMinWidth" format="dimension" /> + + <!-- The maximum image width. --> + <attr name="imageMaxWidth" format="dimension" /> + + <!-- The minimum image height. --> + <attr name="imageMinHeight" format="dimension" /> + + <!-- The maximum image height. --> + <attr name="imageMaxHeight" format="dimension" /> + </declare-styleable> + + <!-- Styleable for configuring the car progress bar --> + <declare-styleable name="CarProgressBar"> + <!-- The minimum image size. --> + <attr name="imageMinSize" format="dimension" /> + + <!-- The maximum image size. --> + <attr name="imageMaxSize" format="dimension" /> + </declare-styleable> + + <!-- Custom error state to be used in edit boxes or other components that support this state --> + <declare-styleable name="ErrorState"> + <attr name="state_error" format="boolean"/> + </declare-styleable> +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/values/integers.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/values/integers.xml new file mode 100644 index 0000000..ac16615 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/values/integers.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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> + <integer name="action_strip_animation_duration_millis">250</integer> +</resources> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/ActionButtonListViewHelper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/ActionButtonListViewHelper.java new file mode 100644 index 0000000..7558316 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/ActionButtonListViewHelper.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common.testing; + +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView; +import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonView; + +/** Test helper for the action button list view. */ +public class ActionButtonListViewHelper { + private static final int LAYOUT_WIDTH = 400; + private static final int LAYOUT_HEIGHT = 600; + + private final ViewGroup mActionButtonListView; + + public ActionButtonListViewHelper(ViewGroup actionButtonListView) { + mActionButtonListView = actionButtonListView; + } + + /** Force a measure and layout for the action strip. */ + public void measureAndLayout() { + mActionButtonListView.measure( + MeasureSpec.makeMeasureSpec(LAYOUT_WIDTH, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(LAYOUT_HEIGHT, MeasureSpec.EXACTLY)); + mActionButtonListView.layout(0, 0, LAYOUT_WIDTH, LAYOUT_HEIGHT); + } + + /** Returns a {@link ActionButtonView} at {@code index} in the {@code mActionButtonListView} */ + public ActionButtonView getAction(int index) { + return (ActionButtonView) mActionButtonListView.getChildAt(index); + } + + /** Returns an {@link ActionButtonListView} instance of the {@code mActionButtonListView} */ + public ActionButtonListView getActionButtonListView() { + return (ActionButtonListView) mActionButtonListView; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/ActionStripHelper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/ActionStripHelper.java new file mode 100644 index 0000000..ebee699 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/ActionStripHelper.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common.testing; + +import static android.view.View.VISIBLE; + +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import com.android.car.libraries.templates.host.view.widgets.common.FabView; +import java.util.ArrayList; +import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Test helper for the action strip. */ +public class ActionStripHelper { + private static final int LAYOUT_WIDTH = 400; + private static final int LAYOUT_HEIGHT = 600; + + private final ViewGroup mActionStripView; + + public ActionStripHelper(ViewGroup actionStripView) { + mActionStripView = actionStripView; + } + + /** Force a measure and layout for the action strip. */ + public void measureAndLayout() { + mActionStripView.measure( + MeasureSpec.makeMeasureSpec(LAYOUT_WIDTH, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(LAYOUT_HEIGHT, MeasureSpec.EXACTLY)); + mActionStripView.layout(0, 0, LAYOUT_WIDTH, LAYOUT_HEIGHT); + } + + /** Returns a {@link List} of {@link FabView}s from the action strip. */ + @Nullable + public List<FabView> getFabViews() { + List<FabView> views = new ArrayList<>(); + for (int i = 0; i < mActionStripView.getChildCount(); i++) { + FabView fabView = (FabView) mActionStripView.getChildAt(i); + if (fabView.getVisibility() == VISIBLE) { + views.add((FabView) mActionStripView.getChildAt(i)); + } + } + return views; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/GridContentViewHelper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/GridContentViewHelper.java new file mode 100644 index 0000000..0aef733 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/GridContentViewHelper.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common.testing; + +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.LayoutManager; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.view.widgets.common.ContentView; +import com.android.car.libraries.templates.host.view.widgets.common.GridAdapter; +import com.android.car.libraries.templates.host.view.widgets.common.GridItemView; +import com.android.car.libraries.templates.host.view.widgets.common.GridRowWrapper; +import com.android.car.libraries.templates.host.view.widgets.common.GridView; +import com.android.car.libraries.templates.host.view.widgets.common.GridWrapper; +import com.android.car.ui.recyclerview.CarUiRecyclerView; +import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Test helper for {@link ContentView} that has the {@link GridWrapper} content set. */ +public class GridContentViewHelper { + private static final int LAYOUT_WIDTH = 400; + private static final int LAYOUT_HEIGHT = 600; + + private final ContentView mContentView; + + public GridContentViewHelper(ContentView contentView) { + mContentView = contentView; + } + + /** Force a measure and layout for the content view. */ + public void measureAndLayout() { + CarUiRecyclerView pagedListView = getRecyclerView(); + if (pagedListView != null) { + pagedListView + .getView() + .measure( + MeasureSpec.makeMeasureSpec(LAYOUT_WIDTH, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(LAYOUT_HEIGHT, MeasureSpec.EXACTLY)); + pagedListView.getView().layout(0, 0, LAYOUT_WIDTH, LAYOUT_HEIGHT); + } + } + + /** Returns the {@link RecyclerView} from the content view. */ + @Nullable + public CarUiRecyclerView getRecyclerView() { + GridView gridView = getGridView(); + if (gridView == null) { + return null; + } + return (CarUiRecyclerView) gridView.findViewById(R.id.grid_paged_list_view); + } + + /** Returns a {@link List} of {@link GridRowWrapper}s from the content view. */ + @Nullable + public List<GridRowWrapper> getGridRowWrappers() { + CarUiRecyclerView listView = getRecyclerView(); + if (listView != null) { + GridAdapter adapter = (GridAdapter) listView.getAdapter(); + if (adapter != null) { + return adapter.getRowWrappers(); + } + } + return null; + } + + /** Returns a specified {@link GridItemView} from the content view. */ + @Nullable + public GridItemView getGridItemView(int index) { + CarUiRecyclerView listView = getRecyclerView(); + if (listView == null) { + return null; + } + + return (GridItemView) listView.getRecyclerViewChildAt(index); + } + + /** Returns a specified {@link GridItemViewHelper} from the content view. */ + @Nullable + public GridItemViewHelper getGridItemViewHelper(int index) { + GridItemView gridItemView = getGridItemView(index); + if (gridItemView == null) { + return null; + } + return new GridItemViewHelper(gridItemView); + } + + @Nullable + private GridView getGridView() { + ViewGroup container = mContentView.findViewById(R.id.container); + if (container == null) { + return null; + } + return (GridView) container.getChildAt(0); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/GridItemViewHelper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/GridItemViewHelper.java new file mode 100644 index 0000000..2558b66 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/GridItemViewHelper.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common.testing; + +import static android.view.View.VISIBLE; + +import android.widget.ImageView; +import android.widget.ProgressBar; +import androidx.annotation.Nullable; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.view.widgets.common.GridItemView; +import com.android.car.ui.widget.CarUiTextView; + +/** Test helper for {@link GridItemView}. */ +public class GridItemViewHelper { + private final GridItemView mGridItemView; + + public GridItemViewHelper(GridItemView gridItemView) { + mGridItemView = gridItemView; + } + + /** Returns the title as a {@link String} for the {@link GridItemView}. */ + @Nullable + public String getTitle() { + return getTextById(R.id.grid_item_title); + } + + /** Returns the title as the raw {@link CharSequence} for the {@link GridItemView}. */ + @Nullable + public CharSequence getText() { + return getTextById(R.id.grid_item_text); + } + + @Nullable + private String getTextById(int id) { + CarUiTextView carUiTextView = mGridItemView.findViewById(id); + if (carUiTextView.getVisibility() == VISIBLE) { + CharSequence title = carUiTextView.getText(); + if (title != null) { + return title.toString(); + } + } + + return null; + } + + /** Returns the {@link ImageView} for the {@link GridItemView}. */ + @Nullable + public ImageView getImage() { + ImageView imageView = mGridItemView.findViewById(R.id.grid_item_image); + if (imageView.getVisibility() == VISIBLE) { + return imageView; + } + return null; + } + + /** Returns the {@link ProgressBar} for the {@link GridItemView}. */ + @Nullable + public ProgressBar getLoadingView() { + ProgressBar loadingView = mGridItemView.findViewById(R.id.grid_item_progress_bar); + if (loadingView.getVisibility() == VISIBLE) { + return loadingView; + } + return null; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/RowListContentViewHelper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/RowListContentViewHelper.java new file mode 100644 index 0000000..877649f --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/RowListContentViewHelper.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common.testing; + +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.Nullable; +import com.android.car.libraries.apphost.template.view.model.RowListWrapper; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.view.widgets.common.ContentView; +import com.android.car.libraries.templates.host.view.widgets.common.RowAdapter; +import com.android.car.libraries.templates.host.view.widgets.common.RowHolder; +import com.android.car.libraries.templates.host.view.widgets.common.RowListView; +import com.android.car.ui.recyclerview.CarUiRecyclerView; +import java.util.List; + +/** Test helper for {@link ContentView} that has the {@link RowListWrapper} content set. */ +public class RowListContentViewHelper { + private static final int LAYOUT_WIDTH = 400; + private static final int LAYOUT_HEIGHT = 600; + + private final ContentView mContentView; + + public RowListContentViewHelper(ContentView contentView) { + mContentView = contentView; + } + + /** Force a measure and layout on the given {@link ContentView}. */ + public void measureAndLayout() { + measureAndLayout(LAYOUT_WIDTH, LAYOUT_HEIGHT); + } + + /** Force a measure and layout on the given {@link ContentView} with given width and height. */ + public void measureAndLayout(int width, int height) { + RowListView pagedListView = getListView(); + if (pagedListView != null) { + pagedListView.measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); + pagedListView.layout(0, 0, width, height); + } + } + + /** Returns the {@link RowListView} from the content view. */ + @Nullable + public RowListView getListView() { + RowListView listView = getRowListView(); + if (listView == null) { + return null; + } + + CarUiRecyclerView recyclerView = listView.findViewById(R.id.list_view); + return (RowListView) recyclerView.getParent(); + } + + /** Returns a {@link List} of the {@link RowHolder}s in the content view. */ + @Nullable + public List<RowHolder> getRowHolders() { + RowListView listView = getListView(); + if (listView != null) { + RowAdapter adapter = listView.getAdapter(); + if (adapter != null) { + return adapter.getRowHolders(); + } + } + return null; + } + + /** Returns the row view for a given index from the content view. */ + @Nullable + public View getRowView(int index) { + return getListItemView(index, View.class); + } + + /** Returns the text of the given section header from the content view. */ + @Nullable + public String getSectionHeaderText(int index) { + View sectionHeaderView = getSectionHeaderView(index); + if (sectionHeaderView == null) { + return null; + } + TextView view = sectionHeaderView.findViewById(R.id.row_section_header); + return view != null ? view.getText().toString() : null; + } + + /** Returns the {@link TextView} of the given section header from the content view. */ + @Nullable + public View getSectionHeaderView(int index) { + return getListItemView(index, View.class); + } + + /** Returns a {@link RowViewHelper} for the given row index from the content view. */ + @Nullable + public RowViewHelper getRowViewHelper(int index) { + View rowView = getRowView(index); + if (rowView == null) { + return null; + } + return new RowViewHelper(rowView); + } + + @Nullable + private <T> T getListItemView(int index, Class<T> clazz) { + RowListView listView = getListView(); + if (listView != null) { + return clazz.cast(listView.getRecyclerView().getRecyclerViewChildAt(index)); + } + return null; + } + + @Nullable + private RowListView getRowListView() { + ViewGroup container = mContentView.findViewById(R.id.container); + if (container == null) { + return null; + } + return (RowListView) container.getChildAt(0); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/RowViewHelper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/RowViewHelper.java new file mode 100644 index 0000000..4fb4478 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/RowViewHelper.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common.testing; + +import static android.view.View.VISIBLE; + +import android.text.Spanned; +import android.text.SpannedString; +import android.view.View; +import android.widget.ImageView; +import android.widget.RadioButton; +import android.widget.Switch; +import androidx.annotation.Nullable; +import com.android.car.libraries.templates.host.R; +import com.android.car.ui.widget.CarUiTextView; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Test helper for rows of {@code RowListView}. */ +public class RowViewHelper { + private final View mRowView; + + public RowViewHelper(View rowView) { + mRowView = rowView; + } + + /** Returns the title for the row as a {@link String}. */ + @Nullable + public String getTitle() { + CharSequence title = getTitleCharSequence(); + return title == null ? null : title.toString(); + } + + /** Get the title for the row as the direct {@link CharSequence}. */ + @Nullable + public CharSequence getTitleCharSequence() { + return ((CarUiTextView) mRowView.findViewById(R.id.car_ui_list_item_title)).getText(); + } + + /** Get a specified span in the title for a row. */ + @Nullable + public <T> T getTitleSpanAt(int spanIndex, Class<T> clazz) { + CharSequence charSequence = getTitleCharSequence(); + if (charSequence == null) { + return null; + } + return getSpanAt(charSequence, spanIndex, clazz); + } + + /** Returns {@code true} if the radio button for the row is selected. */ + public boolean isRadioButtonSelected() { + RadioButton radioButton = mRowView.findViewById(R.id.car_ui_list_item_radio_button_widget); + return radioButton.isChecked(); + } + + /** Returns the lines for the body rows. */ + @Nullable + public List<String> getTextLines() { + CarUiTextView bodyTextView = getBodyTextView(); + if (bodyTextView == null) { + return null; + } + + String[] lines = bodyTextView.getText().toString().split("\n"); + return Arrays.asList(lines); + } + + /** Returns the specified span within the row text. */ + @Nullable + public <T> T getTextSpanAt(int textIndex, int spanIndex, Class<T> clazz) { + CharSequence text = getTextAt(textIndex); + if (text != null) { + return getSpanAt(text, spanIndex, clazz); + } + return null; + } + + /** Returns a specific the text for a particular view index within the row. */ + @Nullable + public CharSequence getTextAt(int index) { + return getBodyTextLine(index); + } + + /** Returns the max number of lines for a given view within the row. */ + public int getTextMaxLinesAt(int index) { + CarUiTextView carUiTextView = getBodyTextView(); + return carUiTextView == null ? -1 : carUiTextView.getMaxLines(); + } + + /** Returns the image of the caret for the row. */ + @Nullable + public ImageView getCaret() { + return getImageView(R.id.car_ui_list_item_supplemental_icon); + } + + /** Returns the secondary text view for the row if visible. */ + @Nullable + public CarUiTextView getBodyTextView() { + CarUiTextView bodyTextView = mRowView.findViewById(R.id.car_ui_list_item_body); + if (bodyTextView.getVisibility() == VISIBLE) { + return bodyTextView; + } + return null; + } + + /** Returns the radio button for the row if visible. */ + @Nullable + public RadioButton getRadioButton() { + RadioButton radioButton = mRowView.findViewById(R.id.car_ui_list_item_radio_button_widget); + if (radioButton.getVisibility() == VISIBLE) { + return radioButton; + } + return null; + } + + /** Returns the image for the row if visible. */ + @Nullable + public ImageView getImage() { + return getImageView(R.id.car_ui_list_item_icon); + } + + /** Returns the {@link Switch} view for the row. */ + @Nullable + public Switch getToggle() { + Switch toggle = mRowView.findViewById(R.id.car_ui_list_item_switch_widget); + if (toggle.getVisibility() == VISIBLE) { + return toggle; + } + return null; + } + + /** Returns the view containing the row elements. */ + public View getContainer() { + return mRowView; + } + + /** Returns the view that acts as a touch interceptor. */ + public View getTouchInterceptor() { + return mRowView.findViewById(R.id.car_ui_list_item_touch_interceptor); + } + + @Nullable + private ImageView getImageView(int id) { + ImageView imageView = mRowView.findViewById(id); + if (imageView.getVisibility() == VISIBLE) { + return imageView; + } + return null; + } + + @Nullable + private CharSequence getBodyTextLine(int index) { + CarUiTextView bodyTextView = getBodyTextView(); + if (bodyTextView == null) { + return null; + } + CharSequence bodyText = bodyTextView.getText(); + CharSequence[] lines = split(bodyText, "\n"); + return lines[index]; + } + + @Nullable + private <T> T getSpanAt(CharSequence charSequence, int spanIndex, Class<T> clazz) { + SpannedString ss = (SpannedString) charSequence; + T[] spans = ss.getSpans(0, charSequence.length(), clazz); + if (spans == null || spanIndex > spans.length - 1) { + return null; + } + return spans[spanIndex]; + } + + private static CharSequence[] split(CharSequence charSequence, String regex) { + // A short-cut for non-spanned strings. + if (!(charSequence instanceof Spanned)) { + return charSequence.toString().split(regex); + } + + // Hereafter, emulate String.split for CharSequence. + ArrayList<CharSequence> sequences = new ArrayList<>(); + Matcher matcher = Pattern.compile(regex).matcher(charSequence); + int nextStart = 0; + boolean matched = false; + while (matcher.find()) { + sequences.add(charSequence.subSequence(nextStart, matcher.start())); + nextStart = matcher.end(); + matched = true; + } + if (!matched) { + return new CharSequence[] {charSequence}; + } + sequences.add(charSequence.subSequence(nextStart, charSequence.length())); + return sequences.toArray(new CharSequence[0]); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/TemplateViewHelper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/TemplateViewHelper.java new file mode 100644 index 0000000..7efe1f4 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/TemplateViewHelper.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common.testing; + +import android.annotation.SuppressLint; +import com.android.car.libraries.apphost.view.AbstractTemplatePresenter; +import com.android.car.libraries.templates.host.view.TemplateView; + +/** Test helper for {@link TemplateView}. */ +public final class TemplateViewHelper { + + private static final int LAYOUT_WIDTH = 400; + private static final int LAYOUT_HEIGHT = 600; + + /** Force a measure and layout on the given {@link TemplateView}. */ + @SuppressLint("RestrictedApi") + public static void measureAndLayout(TemplateView templateView) { + templateView.measure(LAYOUT_WIDTH, LAYOUT_HEIGHT); + templateView.layout(0, 0, LAYOUT_WIDTH, LAYOUT_HEIGHT); + + // Robolectric creates views without giving it size, causing the view to fail to take input + // focus. + // Set the content view size and restore focus. + AbstractTemplatePresenter presenter = + (AbstractTemplatePresenter) templateView.getCurrentPresenter(); + if (presenter != null) { + presenter.restoreFocus(); + } + } + + private TemplateViewHelper() {} +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/CompactStepView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/CompactStepView.java new file mode 100644 index 0000000..e262778 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/CompactStepView.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.navigation; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; +import android.widget.LinearLayout; +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.car.app.model.CarIcon; +import androidx.car.app.model.CarText; +import androidx.car.app.navigation.model.Maneuver; +import androidx.car.app.navigation.model.Step; +import com.android.car.libraries.apphost.common.CarColorUtils; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.view.common.CarTextParams; +import com.android.car.libraries.apphost.view.common.CarTextUtils; +import com.android.car.libraries.apphost.view.common.ImageUtils; +import com.android.car.libraries.apphost.view.common.ImageViewParams; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.view.widgets.common.CarUiTextUtils; +import com.android.car.ui.widget.CarUiTextView; + +/** + * A view that displays a compact view of a navigation maneuver. + * + * <p>For example it could show just the maneuver description or a description and an icon. + */ +public class CompactStepView extends LinearLayout { + private ImageView mTurnSymbolView; + private CarUiTextView mDescriptionText; + @Nullable private Step mStep; + private int mDescriptionTextDefaultTextColor; + + public CompactStepView(Context context) { + this(context, null); + } + + public CompactStepView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public CompactStepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public CompactStepView( + Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mTurnSymbolView = findViewById(R.id.compact_turn_symbol); + mDescriptionText = findViewById(R.id.compact_description_text); + + mDescriptionTextDefaultTextColor = mDescriptionText.getCurrentTextColor(); + } + + /** Sets the color of the texts in the view. */ + public void setTextColor(@ColorInt int textColor) { + mDescriptionText.setTextColor(textColor); + } + + /** Sets the colors of the texts in he view to their default colors. */ + public void setDefaultTextColor() { + mDescriptionText.setTextColor(mDescriptionTextDefaultTextColor); + } + + /** + * Sets the {@link Step} to be shown. + * + * <p>Setting a {@code null} steo will cause the view to be hidden. + */ + public void setStep( + TemplateContext templateContext, + @Nullable Step step, + CarTextParams carTextParams, + @ColorInt int cardBackgroundColor) { + L.v(LogTags.TEMPLATE, "Setting compact step view with step: %s", step); + + mStep = step; + if (step == null) { + setVisibility(GONE); + return; + } + Maneuver maneuver = step.getManeuver(); + CarIcon turnIcon = maneuver == null ? null : maneuver.getIcon(); + boolean shouldShowTurnIcon = + ImageUtils.setImageSrc( + templateContext, + turnIcon, + mTurnSymbolView, + ImageViewParams.builder() + .setBackgroundColor(cardBackgroundColor) + .setIgnoreAppTint( + !CarColorUtils.checkIconTintContrast( + templateContext, turnIcon, cardBackgroundColor)) + .build()); + mTurnSymbolView.setVisibility(shouldShowTurnIcon ? VISIBLE : GONE); + + CarTextParams.Builder paramsBuilder = + CarTextParams.builder(carTextParams).setBackgroundColor(cardBackgroundColor); + CarText cue = step.getCue(); + if (cue != null) { + paramsBuilder.setIgnoreAppIconTint( + !CarTextUtils.checkColorContrast(templateContext, cue, cardBackgroundColor)); + } + + mDescriptionText.setText( + CarUiTextUtils.fromCarText( + templateContext, cue, paramsBuilder.build(), mDescriptionText.getMaxLines())); + setVisibility(VISIBLE); + } + + @VisibleForTesting + @Nullable + public Step getStep() { + return mStep; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/DetailedStepView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/DetailedStepView.java new file mode 100644 index 0000000..29c9570 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/DetailedStepView.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.navigation; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; +import androidx.annotation.StyleableRes; +import androidx.annotation.VisibleForTesting; +import androidx.car.app.model.CarIcon; +import androidx.car.app.model.CarText; +import androidx.car.app.model.Distance; +import androidx.car.app.navigation.model.Maneuver; +import androidx.car.app.navigation.model.Step; +import com.android.car.libraries.apphost.common.CarColorUtils; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.view.common.CarTextParams; +import com.android.car.libraries.apphost.view.common.CarTextUtils; +import com.android.car.libraries.apphost.view.common.DistanceUtils; +import com.android.car.libraries.apphost.view.common.ImageUtils; +import com.android.car.libraries.apphost.view.common.ImageViewParams; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.view.widgets.common.CarUiTextUtils; +import com.android.car.ui.widget.CarUiTextView; + +/** + * A view that displays a detailed navigation step. + * + * <p>This view tries to display all the elements of a {@link Step} and {@link Distance}. For + * example if available, it would show a turn icon, description and lanes image. It could be used + * with another view to show the next turn. + */ +public class DetailedStepView extends LinearLayout { + private ImageView mTurnSymbolView; + private CarUiTextView mDistanceText; + private CarUiTextView mDescriptionText; + private ImageView mLanesImageView; + private LinearLayout mTurnContainerView; + private FrameLayout mLanesImageContainerView; + private final int mNavCardPaddingVertical; + private final int mNavCardSmallPaddingVertical; + private int mDistanceTextDefaultTextColor; + private int mDescriptionTextDefaultTextColor; + + @Nullable private Step mStep; + @Nullable private Distance mDistance; + + public DetailedStepView(Context context) { + this(context, null); + } + + public DetailedStepView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public DetailedStepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + @StyleableRes + final int[] themeAttrs = { + R.attr.templateNavCardPaddingVertical, + R.attr.templateNavCardSmallPaddingVertical + }; + TypedArray ta = context.obtainStyledAttributes(themeAttrs); + mNavCardPaddingVertical = ta.getDimensionPixelSize(0, 0); + mNavCardSmallPaddingVertical = ta.getDimensionPixelSize(1, 0); + ta.recycle(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mTurnSymbolView = findViewById(R.id.turn_symbol); + mDistanceText = findViewById(R.id.distance_text); + mDescriptionText = findViewById(R.id.description_text); + mLanesImageView = findViewById(R.id.lanes_image); + mTurnContainerView = findViewById(R.id.turn_container); + mLanesImageContainerView = findViewById(R.id.lanes_image_container); + + mDistanceTextDefaultTextColor = mDistanceText.getCurrentTextColor(); + mDescriptionTextDefaultTextColor = mDescriptionText.getCurrentTextColor(); + } + + /** Sets the color of the texts in the view. */ + public void setTextColor(@ColorInt int textColor) { + mDistanceText.setTextColor(textColor); + mDescriptionText.setTextColor(textColor); + } + + /** Sets the colors of the texts in the view to their default colors. */ + public void setDefaultTextColor() { + mDistanceText.setTextColor(mDistanceTextDefaultTextColor); + mDescriptionText.setTextColor(mDescriptionTextDefaultTextColor); + } + + /** + * Sets the {@link Step} and {@link Distance} to be shown. + * + * <p>If the {@link Step} is {@code null} then the entire view is hidden. If the {@link Distance} + * is null then the just the distance text is hidden and the step is still shown. + */ + public void setStepAndDistance( + TemplateContext templateContext, + @Nullable Step step, + @Nullable Distance distance, + CarTextParams cueTextParams, + @ColorInt int cardBackgroundColor, + boolean hideLaneImages) { + L.v( + LogTags.TEMPLATE, + "Setting detailed step view with step: %s, and distance: %s", + step, + distance); + + mStep = step; + if (step == null) { + setVisibility(GONE); + return; + } + mDistance = distance; + Maneuver maneuver = step.getManeuver(); + CarIcon turnIcon = maneuver == null ? null : maneuver.getIcon(); + ImageViewParams turnIconParams = + ImageViewParams.builder() + .setBackgroundColor(cardBackgroundColor) + .setIgnoreAppTint( + !CarColorUtils.checkIconTintContrast( + templateContext, turnIcon, cardBackgroundColor)) + .build(); + boolean shouldShowTurnIcon = + ImageUtils.setImageSrc(templateContext, turnIcon, mTurnSymbolView, turnIconParams); + mTurnSymbolView.setVisibility(shouldShowTurnIcon ? VISIBLE : GONE); + + if (distance != null) { + mDistanceText.setText( + CarUiTextUtils.fromCharSequence( + templateContext, + DistanceUtils.convertDistanceToDisplayString(templateContext, distance), + mDistanceText.getMaxLines())); + mDistanceText.setVisibility(VISIBLE); + } else { + mDistanceText.setVisibility(GONE); + } + + CarText cue = step.getCue(); + if (cue == null || CarText.isNullOrEmpty(cue)) { + mDescriptionText.setVisibility(GONE); + } else { + // Ignore app icon tint if it does not pass color contrast check + cueTextParams = + CarTextParams.builder(cueTextParams) + .setBackgroundColor(cardBackgroundColor) + .setIgnoreAppIconTint( + !CarTextUtils.checkColorContrast(templateContext, cue, cardBackgroundColor)) + .build(); + mDescriptionText.setText( + CarUiTextUtils.fromCarText( + templateContext, cue, cueTextParams, mDescriptionText.getMaxLines())); + mDescriptionText.setVisibility(VISIBLE); + } + + CarIcon laneImage = step.getLanesImage(); + ImageViewParams laneImageParams = + ImageViewParams.builder() + .setBackgroundColor(cardBackgroundColor) + .setIgnoreAppTint( + !CarColorUtils.checkIconTintContrast( + templateContext, laneImage, cardBackgroundColor)) + .build(); + + boolean shouldShowLanesImage = + !hideLaneImages + && ImageUtils.setImageSrc(templateContext, laneImage, mLanesImageView, laneImageParams); + + int turnContainerBottomMargin; + if (shouldShowLanesImage) { + mLanesImageContainerView.setVisibility(VISIBLE); + + // If the lane image is present, apply the small internal padding between the turn + // container and the lane image. + turnContainerBottomMargin = mNavCardSmallPaddingVertical; + } else { + mLanesImageContainerView.setVisibility(GONE); + turnContainerBottomMargin = mNavCardPaddingVertical; + } + LinearLayout.LayoutParams layoutParams = + (LinearLayout.LayoutParams) mTurnContainerView.getLayoutParams(); + layoutParams.bottomMargin = turnContainerBottomMargin; + mTurnContainerView.setLayoutParams(layoutParams); + + setVisibility(VISIBLE); + } + + @VisibleForTesting + @Nullable + public Step getStep() { + return mStep; + } + + @VisibleForTesting + @Nullable + public Distance getDistance() { + return mDistance; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/MessageView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/MessageView.java new file mode 100644 index 0000000..1e7bccd --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/MessageView.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.navigation; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; +import android.widget.LinearLayout; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.car.app.model.CarIcon; +import androidx.car.app.model.CarText; +import com.android.car.libraries.apphost.common.CarColorUtils; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.view.common.ImageUtils; +import com.android.car.libraries.apphost.view.common.ImageViewParams; +import com.android.car.libraries.templates.host.R; +import com.android.car.libraries.templates.host.view.widgets.common.CarUiTextUtils; +import com.android.car.ui.widget.CarUiTextView; + +/** A view that displays a message with optional image and subtext. */ +public class MessageView extends LinearLayout { + private ImageView mImageView; + private CarUiTextView mTitleView; + private CarUiTextView mTextView; + private int mTitleDefaultTextColor; + private int mTextDefaultTextColor; + + public MessageView(@NonNull Context context) { + this(context, null); + } + + public MessageView(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + @SuppressWarnings({"argument.type.incompatible"}) + public MessageView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mImageView = findViewById(R.id.message_image); + mTitleView = findViewById(R.id.message_title); + mTextView = findViewById(R.id.message_text); + + mTitleDefaultTextColor = mTitleView.getCurrentTextColor(); + mTextDefaultTextColor = mTextView.getCurrentTextColor(); + } + + /** Sets the color of the texts in the view. */ + public void setTextColor(@ColorInt int textColor) { + mTitleView.setTextColor(textColor); + mTextView.setTextColor(textColor); + } + + /** Sets the colors of the texts in the view to their default colors. */ + public void setDefaultTextColor() { + mTitleView.setTextColor(mTitleDefaultTextColor); + mTextView.setTextColor(mTextDefaultTextColor); + } + + /** Sets the title, image and text content the view. */ + public void setMessage( + TemplateContext templateContext, + @Nullable CarIcon image, + CarText title, + @Nullable CarText text, + @ColorInt int cardBackgroundColor) { + L.v( + LogTags.TEMPLATE, + "Setting message view with message: %s secondary: %s image: %s", + title, + text, + image); + + boolean shouldShowImage = + ImageUtils.setImageSrc( + templateContext, + image, + mImageView, + ImageViewParams.builder() + .setBackgroundColor(cardBackgroundColor) + .setIgnoreAppTint( + !CarColorUtils.checkIconTintContrast( + templateContext, image, cardBackgroundColor)) + .build()); + mImageView.setVisibility(shouldShowImage ? VISIBLE : GONE); + + mTitleView.setText( + CarUiTextUtils.fromCarText(templateContext, title, mTitleView.getMaxLines())); + + mTextView.setText(CarUiTextUtils.fromCarText(templateContext, text, mTextView.getMaxLines())); + mTextView.setVisibility(!CarText.isNullOrEmpty(text) ? VISIBLE : GONE); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/ProgressView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/ProgressView.java new file mode 100644 index 0000000..bb07bdd --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/ProgressView.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.navigation; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.util.AttributeSet; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.android.car.libraries.templates.host.R; + +/** A view that displays a progress indicator. */ +public class ProgressView extends LinearLayout { + private ProgressBar mProgressBar; + + public ProgressView(@NonNull Context context) { + this(context, null); + } + + public ProgressView(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + @SuppressWarnings({"argument.type.incompatible"}) + public ProgressView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mProgressBar = findViewById(R.id.progress_indicator); + } + + /** Sets the color of the progress indicator. */ + public void setColor(@ColorInt int color) { + mProgressBar.setIndeterminateTintList(ColorStateList.valueOf(color)); + } + + /** Sets the color of the progress indicator to its default color. */ + public void setDefaultColor() { + mProgressBar.setIndeterminateTintList(null); + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/TravelEstimateView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/TravelEstimateView.java new file mode 100644 index 0000000..dd9e99a --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/TravelEstimateView.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2021 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.libraries.templates.host.view.widgets.navigation; + +import static android.graphics.Color.TRANSPARENT; + +import android.content.Context; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.ForegroundColorSpan; +import android.util.AttributeSet; +import android.widget.LinearLayout; +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.car.app.model.DateTimeWithZone; +import androidx.car.app.model.Distance; +import androidx.car.app.navigation.model.TravelEstimate; +import com.android.car.libraries.apphost.common.CarColorUtils; +import com.android.car.libraries.apphost.common.TemplateContext; +import com.android.car.libraries.apphost.distraction.constraints.CarColorConstraints; +import com.android.car.libraries.apphost.logging.L; +import com.android.car.libraries.apphost.logging.LogTags; +import com.android.car.libraries.apphost.view.common.DateTimeUtils; +import com.android.car.libraries.apphost.view.common.DistanceUtils; +import com.android.car.libraries.templates.host.R; +import com.android.car.ui.widget.CarUiTextView; +import java.time.Duration; +import java.time.ZoneId; +import java.util.ArrayList; + +/** + * A view that displays a travel estimate for the navigation trip. + * + * <p>This view tries to display elements from the {@link TravelEstimate} data. For example if + * available, it would show the estimated time of arrival and distance to destination. + */ +public class TravelEstimateView extends LinearLayout { + private static final String INTERPUNCT = "\u00b7"; + private static final String TIME_AND_DISTANCE_SEPARATOR = " " + INTERPUNCT + " "; + + private CarUiTextView mArrivalTimeText; + private CarUiTextView mTimeAndDistanceText; + @Nullable private TravelEstimate mTravelEstimate; + + public TravelEstimateView(Context context) { + this(context, null); + } + + public TravelEstimateView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public TravelEstimateView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public TravelEstimateView( + Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mArrivalTimeText = findViewById(R.id.arrival_time_text); + mTimeAndDistanceText = findViewById(R.id.time_and_distance_text); + } + + /** Sets the {@link TravelEstimate} or hides the view if set to {@code null} */ + @SuppressWarnings("NewApi") // java.time APIs are OK through de-sugaring. + public void setTravelEstimate( + TemplateContext templateContext, @Nullable TravelEstimate travelEstimate) { + L.v(LogTags.TEMPLATE, "Setting travel estimate view: %s", travelEstimate); + + mTravelEstimate = travelEstimate; + if (travelEstimate == null) { + setVisibility(GONE); + return; + } + + // Display the arrival time. + DateTimeWithZone arrivalTime = travelEstimate.getArrivalTimeAtDestination(); + if (arrivalTime != null) { + mArrivalTimeText.setText( + DateTimeUtils.formatArrivalTimeString( + templateContext, arrivalTime, ZoneId.systemDefault())); + } else { + // This shouldn't happen since the API should enforce a non-null arrival time. + mArrivalTimeText.setText(new ArrayList<>()); + } + + // Display the remaining trip time. + // The destination travel estimate's duration should not be unknown, but if it is, use an + // empty + // string. + long remainingTimeSeconds = travelEstimate.getRemainingTimeSeconds(); + String timeString = + remainingTimeSeconds == TravelEstimate.REMAINING_TIME_UNKNOWN + ? "" + : DateTimeUtils.formatDurationString( + templateContext, Duration.ofSeconds(remainingTimeSeconds)); + Distance distance = travelEstimate.getRemainingDistance(); + String distanceString; + if (distance != null) { + distanceString = DistanceUtils.convertDistanceToDisplayString(templateContext, distance); + } else { + distanceString = ""; + L.w(LogTags.TEMPLATE, "Remaining distance for the travel estimate is expected but not set"); + } + String timeAndDistanceString = timeString + TIME_AND_DISTANCE_SEPARATOR + distanceString; + + // If we have a valid custom text color, use it. + SpannableString timeAndDistanceSpannable = new SpannableString(timeAndDistanceString); + + @ColorInt + int remainingTimeColor = + CarColorUtils.resolveColor( + templateContext, + travelEstimate.getRemainingTimeColor(), + /* isDark= */ false, + /* defaultColor= */ TRANSPARENT, + CarColorConstraints.STANDARD_ONLY); + setStringColorSpan(remainingTimeColor, timeAndDistanceSpannable, 0, timeString.length()); + + @ColorInt + int remainingDistanceColor = + CarColorUtils.resolveColor( + templateContext, + travelEstimate.getRemainingDistanceColor(), + /* isDark= */ false, + /* defaultColor= */ TRANSPARENT, + CarColorConstraints.STANDARD_ONLY); + setStringColorSpan( + remainingDistanceColor, + timeAndDistanceSpannable, + timeString.length() + TIME_AND_DISTANCE_SEPARATOR.length(), + timeAndDistanceString.length()); + + mTimeAndDistanceText.setText(timeAndDistanceSpannable); + } + + /** Sets a color span in the given {@link SpannableString}. */ + private static void setStringColorSpan( + @ColorInt int color, SpannableString spannable, int start, int end) { + if (color != TRANSPARENT) { + spannable.setSpan( + new ForegroundColorSpan(color), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + @VisibleForTesting + @Nullable + public TravelEstimate getTravelEstimate() { + return mTravelEstimate; + } +} diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/compact_step_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/compact_step_view.xml new file mode 100644 index 0000000..27f04d4 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/compact_step_view.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<com.android.car.libraries.templates.host.view.widgets.navigation.CompactStepView + 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="wrap_content" + android:paddingVertical="?templateNavCardSmallPaddingVertical" + android:paddingHorizontal="?templateNavCardPaddingHorizontal" + android:orientation="horizontal" + android:gravity="center_vertical"> + + <!-- An image showing the turn icon on the left of the view. --> + <com.android.car.libraries.templates.host.view.widgets.common.CarImageView + android:id="@+id/compact_turn_symbol" + android:layout_width="?templateNavCardSmallImageSize" + android:layout_height="?templateNavCardSmallImageSize" + app:imageMinWidth="?templateNavCardSmallImageSizeMin" + app:imageMaxWidth="?templateNavCardSmallImageSizeMax" + app:imageMinHeight="?templateNavCardSmallImageSizeMin" + app:imageMaxHeight="?templateNavCardSmallImageSizeMax" + android:scaleType="fitCenter" + android:adjustViewBounds="true" + android:layout_gravity="center" + tools:ignore="ContentDescription" /> + + <!-- A text view displaying the description of the step, e.g. "Boggle St". --> + <CarUiTextView + android:id="@+id/compact_description_text" + style="?templateRoutingCompactDescriptionStyle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/template_steps_card_image_to_text_spacing_vertical" + android:layout_gravity="start|center" /> +</com.android.car.libraries.templates.host.view.widgets.navigation.CompactStepView> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/detailed_step_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/detailed_step_view.xml new file mode 100644 index 0000000..c35eab7 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/detailed_step_view.xml @@ -0,0 +1,99 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<com.android.car.libraries.templates.host.view.widgets.navigation.DetailedStepView + 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:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <!-- A container for the turn icon, the distance, and the description. + We use this container so that we can apply the right margins to this + content. --> + <LinearLayout + android:id="@+id/turn_container" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="?templateNavCardPaddingHorizontal" + android:layout_marginVertical="?templateNavCardPaddingVertical"> + + <!-- The top row showing the turn icon and the distance to it. --> + <LinearLayout + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical"> + + <!-- An image showing the turn icon on the top left of the view. --> + <com.android.car.libraries.templates.host.view.widgets.common.CarImageView + android:id="@+id/turn_symbol" + android:layout_width="?templateNavCardLargeImageSize" + android:layout_height="?templateNavCardLargeImageSize" + app:imageMinWidth="?templateNavCardLargeImageSizeMin" + app:imageMaxWidth="?templateNavCardLargeImageSizeMax" + app:imageMinHeight="?templateNavCardLargeImageSizeMin" + app:imageMaxHeight="?templateNavCardLargeImageSizeMax" + android:scaleType="fitCenter" + android:adjustViewBounds="true" + tools:ignore="ContentDescription" + android:layout_marginEnd="?templateRoutingStepsCardIconToDistanceSpacingHorizontal" /> + + <!-- A text view next to the turn image on top showing the distance to the + next step. --> + <CarUiTextView + android:id="@+id/distance_text" + style="?templateRoutingDistanceStyle" + android:layout_gravity="center_vertical|start" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + </LinearLayout> + + <!-- A text view displaying the description of the step, e.g. "Turn right + at Morning Roll Ave S". --> + <CarUiTextView + android:id="@+id/description_text" + style="?templateRoutingDescriptionStyle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="?templateNavCardSmallPaddingVertical"/> + </LinearLayout> + + <!-- The image that displays the lanes (e.g. a series of arrows laid + horizontally. --> + <FrameLayout + android:id="@+id/lanes_image_container" + android:background="?templateRoutingLanesImageBackgroundColor" + android:layout_width="match_parent" + android:layout_height="?templateRoutingLanesImageContainerHeight" + android:paddingVertical="?templateRoutingLanesImageContainerVerticalPadding" + android:paddingHorizontal="?templateRoutingLanesImageContainerHorizontalPadding"> + <ImageView + android:id="@+id/lanes_image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:clickable="false" + android:focusable="false" + android:scaleType="fitCenter" + android:adjustViewBounds="true" + tools:ignore="ContentDescription" /> + </FrameLayout> + +</com.android.car.libraries.templates.host.view.widgets.navigation.DetailedStepView> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/message_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/message_view.xml new file mode 100644 index 0000000..eaf1701 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/message_view.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<com.android.car.libraries.templates.host.view.widgets.navigation.MessageView + 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:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:paddingHorizontal="?templateNavCardPaddingHorizontal" + android:paddingVertical="?templateNavCardPaddingVertical"> + + <!-- An image showing the destination image. --> + <com.android.car.libraries.templates.host.view.widgets.common.CarImageView + android:id="@+id/message_image" + android:layout_width="?templateNavCardLargeImageSize" + android:layout_height="?templateNavCardLargeImageSize" + app:imageMinWidth="?templateNavCardLargeImageSizeMin" + app:imageMaxWidth="?templateNavCardLargeImageSizeMax" + app:imageMinHeight="?templateNavCardLargeImageSizeMin" + app:imageMaxHeight="?templateNavCardLargeImageSizeMax" + android:layout_gravity="start" + android:scaleType="fitCenter" + android:adjustViewBounds="true" + android:visibility="gone" + tools:ignore="ContentDescription" /> + + <!-- A title on top of the card, e.g. the name of the location the + user arrived at. --> + <CarUiTextView + android:id="@+id/message_title" + style="?templateRoutingMessagePrimaryStyle" + android:layout_gravity="start" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="?templateRoutingMessageInnerPaddingVertical" /> + + <!-- A text view displaying the address of the location. --> + <CarUiTextView + android:id="@+id/message_text" + style="?templateRoutingMessageSecondaryStyle" + android:layout_gravity="start" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="?templateRoutingMessageInnerPaddingVertical" /> +</com.android.car.libraries.templates.host.view.widgets.navigation.MessageView> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/progress_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/progress_view.xml new file mode 100644 index 0000000..1aa46e2 --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/progress_view.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<com.android.car.libraries.templates.host.view.widgets.navigation.ProgressView + 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:layout_marginHorizontal="?templateNavCardPaddingHorizontal" + android:layout_marginVertical="?templateNavCardPaddingVertical" + android:layout_gravity="center" + android:orientation="vertical" + android:visibility="gone"> + <com.android.car.libraries.templates.host.view.widgets.common.CarProgressBar + android:id="@+id/progress_indicator" + style="?templateLoadingSpinnerStyle" + android:layout_width="?templateNavCardLargeImageSize" + android:layout_height="?templateNavCardLargeImageSize" + app:imageMinSize="?templateNavCardLargeImageSizeMin" + app:imageMaxSize="?templateNavCardLargeImageSizeMax" + android:layout_gravity="center_horizontal" /> + +</com.android.car.libraries.templates.host.view.widgets.navigation.ProgressView> diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/travel_estimate_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/travel_estimate_view.xml new file mode 100644 index 0000000..88ae5de --- /dev/null +++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/travel_estimate_view.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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 + + https://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. +--> + +<com.android.car.libraries.templates.host.view.widgets.navigation.TravelEstimateView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="start|center_vertical"> + + <!-- The first row showing the arrival time. --> + <CarUiTextView + android:id="@+id/arrival_time_text" + style="?templateRoutingTravelEstimateStyle" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + + <!-- The first row showing the remaining time and distance. --> + <CarUiTextView + android:id="@+id/time_and_distance_text" + style="?templateRoutingTravelEstimateStyle" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> +</com.android.car.libraries.templates.host.view.widgets.navigation.TravelEstimateView> |