Merge "Add KitchenSink entry to enable/disable rotary" into rvc-qpr-dev
diff --git a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
index f8f4290..1c40b88 100644
--- a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
+++ b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
@@ -105,6 +105,9 @@
<uses-permission android:name="android.car.permission.CONTROL_CAR_FEATURES"/>
+ <!-- use for rotary fragment to enable/disable packages related to rotary -->
+ <uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE"/>
+
<application android:label="@string/app_title"
android:icon="@drawable/ic_launcher">
<uses-library android:name="android.test.runner"/>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/rotary_fragment.xml b/tests/EmbeddedKitchenSinkApp/res/layout/rotary_fragment.xml
new file mode 100644
index 0000000..e1f739a
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/rotary_fragment.xml
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.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.
+ -->
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fadeScrollbars="false">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/unavailable_message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="CarRotaryService does not exist in this build"
+ android:textSize="30sp"
+ android:textColor="@*android:color/red"/>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="30sp"
+ android:text="Overall Controls"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/enable_all_rotary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp"
+ android:text="Enable All"
+ android:textSize="30sp"/>
+
+ <Button
+ android:id="@+id/disable_all_rotary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp"
+ android:text="Disable All"
+ android:textSize="30sp"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_margin="10dp">
+
+ <TextView
+ android:layout_width="500dp"
+ android:layout_height="wrap_content"
+ android:textSize="30sp"
+ android:text="CarRotaryService state"/>
+
+ <Switch
+ android:id="@+id/rotary_service_toggle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="30sp"
+ android:layout_gravity="center"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_margin="10dp">
+
+ <TextView
+ android:layout_width="500dp"
+ android:layout_height="wrap_content"
+ android:textSize="30sp"
+ android:text="RotaryIME state"/>
+
+ <Switch
+ android:id="@+id/rotary_ime_toggle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="30sp"
+ android:layout_gravity="center"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_margin="10dp">
+
+ <TextView
+ android:layout_width="500dp"
+ android:layout_height="wrap_content"
+ android:textSize="30sp"
+ android:text="RotaryPlayground state"/>
+
+ <Switch
+ android:id="@+id/rotary_playground_toggle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="30sp"
+ android:layout_gravity="center"/>
+
+ </LinearLayout>
+ </LinearLayout>
+</ScrollView>
\ No newline at end of file
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
index e883501..0732562 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
@@ -63,6 +63,7 @@
import com.google.android.car.kitchensink.power.PowerTestFragment;
import com.google.android.car.kitchensink.projection.ProjectionFragment;
import com.google.android.car.kitchensink.property.PropertyTestFragment;
+import com.google.android.car.kitchensink.rotary.RotaryFragment;
import com.google.android.car.kitchensink.sensor.SensorsTestFragment;
import com.google.android.car.kitchensink.storagelifetime.StorageLifetimeFragment;
import com.google.android.car.kitchensink.storagevolumes.StorageVolumesFragment;
@@ -190,6 +191,7 @@
new FragmentMenuEntry("profile_user", ProfileUserFragment.class),
new FragmentMenuEntry("projection", ProjectionFragment.class),
new FragmentMenuEntry("property test", PropertyTestFragment.class),
+ new FragmentMenuEntry("rotary", RotaryFragment.class),
new FragmentMenuEntry("sensors", SensorsTestFragment.class),
new FragmentMenuEntry("storage lifetime", StorageLifetimeFragment.class),
new FragmentMenuEntry("storage volumes", StorageVolumesFragment.class),
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/rotary/RotaryFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/rotary/RotaryFragment.java
new file mode 100644
index 0000000..bf13017
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/rotary/RotaryFragment.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.google.android.car.kitchensink.rotary;
+
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.google.android.car.kitchensink.R;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/** Test fragment to enable/disable various components related to RotaryController. */
+public final class RotaryFragment extends Fragment {
+
+ private static final String TAG = RotaryFragment.class.getSimpleName();
+ private static final String ROTARY_CONTROLLER_PACKAGE = "com.android.car.rotary";
+ private static final ComponentName ROTARY_SERVICE_NAME = ComponentName.unflattenFromString(
+ ROTARY_CONTROLLER_PACKAGE + "/com.android.car.rotary.RotaryService");
+ private static final String ROTARY_PLAYGROUND_PACKAGE = "com.android.car.rotaryplayground";
+ private static final String ROTARY_IME_PACKAGE = "com.android.car.rotaryime";
+
+ private static final String ACCESSIBILITY_DELIMITER = ":";
+
+ private static final int ON = 1;
+ private static final int OFF = 0;
+
+ private final IntentFilter mFilter = new IntentFilter();
+
+ private TextView mUnavailableMessage;
+ private Button mEnableAllButton;
+ private Button mDisableAllButton;
+ private Switch mRotaryServiceToggle;
+ private Switch mRotaryImeToggle;
+ private Switch mRotaryPlaygroundToggle;
+
+ private final BroadcastReceiver mPackagesUpdatedReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ switch (action) {
+ case Intent.ACTION_PACKAGE_ADDED:
+ case Intent.ACTION_PACKAGE_CHANGED:
+ case Intent.ACTION_PACKAGE_REMOVED:
+ refreshUi();
+ break;
+ }
+ }
+ };
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ mFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ mFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.rotary_fragment, container, /* attachToRoot= */ false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ mUnavailableMessage = view.findViewById(R.id.unavailable_message);
+
+ mEnableAllButton = view.findViewById(R.id.enable_all_rotary);
+ mDisableAllButton = view.findViewById(R.id.disable_all_rotary);
+
+ mRotaryServiceToggle = view.findViewById(R.id.rotary_service_toggle);
+ mRotaryImeToggle = view.findViewById(R.id.rotary_ime_toggle);
+ mRotaryPlaygroundToggle = view.findViewById(R.id.rotary_playground_toggle);
+
+ mRotaryServiceToggle.setOnCheckedChangeListener(
+ (buttonView, isChecked) -> enableRotaryService(getContext(), isChecked));
+ mRotaryImeToggle.setOnCheckedChangeListener(
+ (buttonView, isChecked) -> enableApplication(getContext(), ROTARY_IME_PACKAGE,
+ isChecked));
+ mRotaryPlaygroundToggle.setOnCheckedChangeListener(
+ (buttonView, isChecked) -> enableApplication(getContext(),
+ ROTARY_PLAYGROUND_PACKAGE, isChecked));
+
+ mEnableAllButton.setOnClickListener(v -> {
+ if (mRotaryServiceToggle.isEnabled()) {
+ mRotaryServiceToggle.setChecked(true);
+ }
+ if (mRotaryImeToggle.isEnabled()) {
+ mRotaryImeToggle.setChecked(true);
+ }
+ if (mRotaryPlaygroundToggle.isEnabled()) {
+ mRotaryPlaygroundToggle.setChecked(true);
+ }
+ });
+
+ mDisableAllButton.setOnClickListener(v -> {
+ if (mRotaryServiceToggle.isEnabled()) {
+ mRotaryServiceToggle.setChecked(false);
+ }
+ if (mRotaryImeToggle.isEnabled()) {
+ mRotaryImeToggle.setChecked(false);
+ }
+ if (mRotaryPlaygroundToggle.isEnabled()) {
+ mRotaryPlaygroundToggle.setChecked(false);
+ }
+ });
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ getContext().registerReceiver(mPackagesUpdatedReceiver, mFilter);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ refreshUi();
+ }
+
+ private void refreshUi() {
+ ApplicationInfo info = findApplication(getContext(), ROTARY_CONTROLLER_PACKAGE);
+ boolean rotaryApplicationExists = info != null;
+ info = findApplication(getContext(), ROTARY_IME_PACKAGE);
+ boolean rotaryImeExists = info != null;
+ info = findApplication(getContext(), ROTARY_PLAYGROUND_PACKAGE);
+ boolean rotaryPlaygroundExists = info != null;
+
+ mUnavailableMessage.setVisibility(!rotaryApplicationExists ? View.VISIBLE : View.GONE);
+
+ mEnableAllButton.setEnabled(rotaryApplicationExists);
+ mDisableAllButton.setEnabled(rotaryApplicationExists);
+ mRotaryServiceToggle.setEnabled(rotaryApplicationExists);
+ mRotaryImeToggle.setEnabled(rotaryApplicationExists && rotaryImeExists);
+ mRotaryPlaygroundToggle.setEnabled(rotaryApplicationExists && rotaryPlaygroundExists);
+
+ mRotaryServiceToggle.setChecked(isRotaryServiceEnabled(getContext()));
+ mRotaryImeToggle.setChecked(
+ rotaryImeExists && isApplicationEnabled(getContext(), ROTARY_IME_PACKAGE));
+ mRotaryPlaygroundToggle.setChecked(
+ rotaryPlaygroundExists && isApplicationEnabled(getContext(),
+ ROTARY_PLAYGROUND_PACKAGE));
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+
+ getContext().unregisterReceiver(mPackagesUpdatedReceiver);
+ }
+
+ @Nullable
+ private static ApplicationInfo findApplication(Context context, String packageName) {
+ PackageManager pm = context.getPackageManager();
+ try {
+ Log.d(TAG, "Searching for: " + packageName);
+ return pm.getApplicationInfoAsUser(packageName, /* flags= */ 0,
+ ActivityManager.getCurrentUser());
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Could not find: " + packageName);
+ return null;
+ }
+ }
+
+ private static boolean isApplicationEnabled(Context context, String packageName) {
+ ApplicationInfo info = findApplication(context, packageName);
+ if (info == null) {
+ return false;
+ }
+
+ return info.enabled;
+ }
+
+ private static void enableApplication(Context context, String packageName, boolean enable) {
+ // Check that the application exists.
+ ApplicationInfo info = findApplication(context, packageName);
+ if (info == null) {
+ Log.e(TAG, "Cannot enable application. " + packageName + " package does not exist");
+ return;
+ }
+
+ PackageManager pm = context.getPackageManager();
+ int currentState = pm.getApplicationEnabledSetting(packageName);
+ int desiredState = enable ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
+
+ boolean isEnabled = currentState == PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
+ if (isEnabled != enable
+ || currentState == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) {
+ Log.d(TAG, "Application state updated for " + packageName + ": " + enable);
+ pm.setApplicationEnabledSetting(packageName, desiredState, /* flags= */ 0);
+ }
+ }
+
+ private static boolean isRotaryServiceEnabled(Context context) {
+ ApplicationInfo info = findApplication(context, ROTARY_CONTROLLER_PACKAGE);
+ if (info == null) {
+ return false;
+ }
+
+ int isAccessibilityEnabled = Settings.Secure.getInt(context.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_ENABLED, OFF);
+ if (isAccessibilityEnabled != ON) {
+ return false;
+ }
+
+ String services = Settings.Secure.getString(context.getContentResolver(),
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
+ if (services == null) {
+ return false;
+ }
+
+ return services.contains(ROTARY_CONTROLLER_PACKAGE);
+ }
+
+ private static void enableRotaryService(Context context, boolean enable) {
+ // Check that the RotaryController exists.
+ ApplicationInfo info = findApplication(context, ROTARY_CONTROLLER_PACKAGE);
+ if (info == null) {
+ Log.e(TAG, "Cannot enable rotary service. " + ROTARY_CONTROLLER_PACKAGE
+ + " package does not exist");
+ return;
+ }
+
+ // Set the list of accessibility services.
+ String services = Settings.Secure.getString(context.getContentResolver(),
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
+ if (services == null) {
+ services = "";
+ }
+ Log.d(TAG, "Current list of accessibility services: " + services);
+
+ String[] servicesArray = services.split(ACCESSIBILITY_DELIMITER);
+ Set<ComponentName> servicesSet = new HashSet<>();
+ for (String service : servicesArray) {
+ ComponentName name = ComponentName.unflattenFromString(service);
+ if (name != null) {
+ servicesSet.add(name);
+ }
+ }
+
+ if (enable) {
+ servicesSet.add(ROTARY_SERVICE_NAME);
+ } else {
+ servicesSet.remove(ROTARY_SERVICE_NAME);
+ }
+
+ StringBuilder builder = new StringBuilder();
+ for (ComponentName service : servicesSet) {
+ if (builder.length() > 0) {
+ builder.append(ACCESSIBILITY_DELIMITER);
+ }
+ builder.append(service.flattenToString());
+ }
+
+ Log.d(TAG, "New list of accessibility services: " + builder);
+ Settings.Secure.putString(context.getContentResolver(),
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+ builder.toString());
+
+ // Set the overall enabled state.
+ int desiredState = enable ? ON : OFF;
+ Settings.Secure.putInt(context.getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED,
+ desiredState);
+ }
+}