From e4fe3bcd751137d7040202a5ff75f51209dfdba0 Mon Sep 17 00:00:00 2001 From: M66B Date: Wed, 6 Jul 2022 20:48:41 +0200 Subject: [PATCH] Car connection --- app/build.gradle | 3 + app/src/fdroid/AndroidManifest.xml | 5 + app/src/github/AndroidManifest.xml | 5 + .../car/app/connection/CarConnection.java | 112 ++++++++++++++++ .../connection/CarConnectionTypeLiveData.java | 120 ++++++++++++++++++ 5 files changed, 245 insertions(+) create mode 100644 app/src/main/java/androidx/car/app/connection/CarConnection.java create mode 100644 app/src/main/java/androidx/car/app/connection/CarConnectionTypeLiveData.java diff --git a/app/build.gradle b/app/build.gradle index 1ae40ad879..0a7538ec8e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -593,6 +593,9 @@ dependencies { // https://mvnrepository.com/artifact/com.github.seancfoley/ipaddress implementation "com.github.seancfoley:ipaddress:$ipaddress_version" + // https://mvnrepository.com/artifact/androidx.car.app/app?repo=google + // implementation "androidx.car.app:app:1.2.0-rc01" + // https://github.com/square/leakcanary // https://square.github.io/leakcanary/getting_started/ // https://mvnrepository.com/artifact/com.squareup.leakcanary/leakcanary-android diff --git a/app/src/fdroid/AndroidManifest.xml b/app/src/fdroid/AndroidManifest.xml index ee3f695cf7..359bd39cd1 100644 --- a/app/src/fdroid/AndroidManifest.xml +++ b/app/src/fdroid/AndroidManifest.xml @@ -97,6 +97,11 @@ + + + + This is used for communication with the car host's content provider on queries for + * connection type. + */ + public static final String CAR_CONNECTION_STATE = "CarConnectionState"; + + /** + * Broadcast action that notifies that the car connection has changed and needs to be updated. + */ + public static final String ACTION_CAR_CONNECTION_UPDATED = + "androidx.car.app.connection.action.CAR_CONNECTION_UPDATED"; + + /** + * Represents the types of connections that exist to a car head unit. + * + * @hide + */ + @IntDef({CONNECTION_TYPE_NOT_CONNECTED, CONNECTION_TYPE_NATIVE, CONNECTION_TYPE_PROJECTION}) + @Retention(RetentionPolicy.SOURCE) + @Target({ElementType.TYPE_USE}) + @RestrictTo(LIBRARY) + public @interface ConnectionType { + } + + /** + * Not connected to any car head unit.z + */ + public static final int CONNECTION_TYPE_NOT_CONNECTED = 0; + + /** + * Natively running on a head unit (Android Automotive OS). + */ + public static final int CONNECTION_TYPE_NATIVE = 1; + + /** + * Connected to a car head unit by projecting to it. + */ + public static final int CONNECTION_TYPE_PROJECTION = 2; + + private final LiveData mConnectionTypeLiveData; + + /** + * Constructs a {@link CarConnection} that can be used to get connection information. + * + * @throws NullPointerException if {@code context} is {@code null} + */ + public CarConnection(@NonNull Context context) { + requireNonNull(context); + mConnectionTypeLiveData = /*isAutomotiveOS(context) + ? new AutomotiveCarConnectionTypeLiveData() + :*/ new CarConnectionTypeLiveData(context); + } + + /** + * Returns a {@link LiveData} that can be observed to get current connection type. + * + *

The recommended pattern is to observe the {@link LiveData} with the activity's + * lifecycle in order to get updates on the state change throughout the activity's lifetime. + * + *

Connection types are: + *

    + *
  1. {@link #CONNECTION_TYPE_NOT_CONNECTED} + *
  2. {@link #CONNECTION_TYPE_NATIVE} + *
  3. {@link #CONNECTION_TYPE_PROJECTION} + *
+ */ + @NonNull + public LiveData<@ConnectionType Integer> getType() { + return mConnectionTypeLiveData; + } + } diff --git a/app/src/main/java/androidx/car/app/connection/CarConnectionTypeLiveData.java b/app/src/main/java/androidx/car/app/connection/CarConnectionTypeLiveData.java new file mode 100644 index 0000000000..e688eb2d22 --- /dev/null +++ b/app/src/main/java/androidx/car/app/connection/CarConnectionTypeLiveData.java @@ -0,0 +1,120 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.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 androidx.car.app.connection; + +import static androidx.car.app.connection.CarConnection.ACTION_CAR_CONNECTION_UPDATED; +import static androidx.car.app.connection.CarConnection.CAR_CONNECTION_STATE; +//import static androidx.car.app.utils.LogTags.TAG_CONNECTION_TO_CAR; + +import android.content.AsyncQueryHandler; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.car.app.connection.CarConnection.ConnectionType; +import androidx.lifecycle.LiveData; + +/** + * A {@link LiveData} that will query once while being observed and only again if it gets updates + * via a broadcast. + */ +final class CarConnectionTypeLiveData extends LiveData<@ConnectionType Integer> { + @VisibleForTesting + static final String CAR_CONNECTION_AUTHORITY = "androidx.car.app.connection"; + + private static final int QUERY_TOKEN = 42; + private static final Uri PROJECTION_HOST_URI = new Uri.Builder().scheme("content").authority( + CAR_CONNECTION_AUTHORITY).build(); + + private final Context mContext; + private final AsyncQueryHandler mQueryHandler; + private final CarConnectionBroadcastReceiver mBroadcastReceiver; + + CarConnectionTypeLiveData(Context context) { + mContext = context; + + mQueryHandler = new CarConnectionQueryHandler( + context.getContentResolver()); + mBroadcastReceiver = new CarConnectionBroadcastReceiver(); + } + + @Override + public void onActive() { + mContext.registerReceiver(mBroadcastReceiver, + new IntentFilter(ACTION_CAR_CONNECTION_UPDATED)); + queryForState(); + } + + @Override + public void onInactive() { + mContext.unregisterReceiver(mBroadcastReceiver); + mQueryHandler.cancelOperation(QUERY_TOKEN); + } + + void queryForState() { + mQueryHandler.startQuery(/* token= */ QUERY_TOKEN, /* cookie= */ null, + /* uri */ PROJECTION_HOST_URI, + /* projection= */ new String[]{CAR_CONNECTION_STATE}, /* selection= */ null, + /* selectionArgs= */ null, /* orderBy= */ null); + } + + class CarConnectionQueryHandler extends AsyncQueryHandler { + CarConnectionQueryHandler(ContentResolver resolver) { + super(resolver); + } + + @Override + protected void onQueryComplete(int token, Object cookie, Cursor response) { + if (response == null) { + //Log.w(TAG_CONNECTION_TO_CAR, "Null response from content provider when checking " + // + "connection to the car, treating as disconnected"); + postValue(CarConnection.CONNECTION_TYPE_NOT_CONNECTED); + return; + } + + int carConnectionTypeColumn = response.getColumnIndex(CAR_CONNECTION_STATE); + if (carConnectionTypeColumn < 0) { + //Log.e(TAG_CONNECTION_TO_CAR, "Connection to car response is missing the " + // + "connection type, treating as disconnected"); + postValue(CarConnection.CONNECTION_TYPE_NOT_CONNECTED); + return; + } + + if (!response.moveToNext()) { + //Log.e(TAG_CONNECTION_TO_CAR, "Connection to car response is empty, treating as " + // + "disconnected"); + postValue(CarConnection.CONNECTION_TYPE_NOT_CONNECTED); + return; + } + + postValue(response.getInt(carConnectionTypeColumn)); + } + } + + class CarConnectionBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + queryForState(); + } + } +}