diff options
author | Keun young Park <keunyoung@google.com> | 2021-07-29 14:44:30 -0700 |
---|---|---|
committer | Keun young Park <keunyoung@google.com> | 2021-08-03 17:09:36 -0700 |
commit | 51773dfd7d367534604406c52c52de95a99a35ca (patch) | |
tree | 9ebac3b7138d67f45007d56739eed530a07e6e59 /service-builtin/src | |
parent | 89026789d664857c14726a8be4ee0d6bd52580c1 (diff) | |
download | Car-51773dfd7d367534604406c52c52de95a99a35ca.tar.gz |
Split car service into two
- directory service-builtin hosts com.android.car which is builtin and
is not in module
* Still module name is CarService
* hosts all permissions, all external facing Services and Activity
- service hosts com.android.car.updatable which is added to mainline module
* Module name is CarServiceUpdatable
* hosts actual service codes to run carservice
* code namespace is still com.android.car
- disable some tests for now:
* ScriptExecutorTest: It should be done differently after
splitting the scriptexecutor.
Bug: 195013206
Test: atest CarServiceTest CarServieUnitTest
Change-Id: I25e6fcfab38c48cca25fe31632f87fe9568dbfb4
Diffstat (limited to 'service-builtin/src')
15 files changed, 2232 insertions, 0 deletions
diff --git a/service-builtin/src/com/android/car/CarService.java b/service-builtin/src/com/android/car/CarService.java new file mode 100644 index 0000000000..a848f6b0d6 --- /dev/null +++ b/service-builtin/src/com/android/car/CarService.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; + +import android.content.Intent; + +/** Proxy service for CarServciceImpl */ +public class CarService extends ServiceProxy { + + public CarService() { + super(UpdatablePackageDependency.CAR_SERVICE_IMPL_CLASS); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + // keep it alive. + return START_STICKY; + } +} diff --git a/service-builtin/src/com/android/car/PerUserCarService.java b/service-builtin/src/com/android/car/PerUserCarService.java new file mode 100644 index 0000000000..6586065e86 --- /dev/null +++ b/service-builtin/src/com/android/car/PerUserCarService.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.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; + +import android.annotation.Nullable; +import android.car.builtin.util.Slogf; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.util.IndentingPrintWriter; + +import com.android.car.admin.PerUserCarDevicePolicyService; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** Proxy service for PerUserCarServiceImpl */ +public class PerUserCarService extends ServiceProxy { + private static final boolean DBG = false; + private static final String TAG = PerUserCarService.class.getSimpleName(); + + private @Nullable PerUserCarDevicePolicyService mPerUserCarDevicePolicyService; + + public PerUserCarService() { + super(UpdatablePackageDependency.PER_USER_CAR_SERVICE_IMPL_CLASS); + } + + @Override + public void onCreate() { + super.onCreate(); // necessary for impl side execution + if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN)) { + mPerUserCarDevicePolicyService = PerUserCarDevicePolicyService.getInstance(this); + mPerUserCarDevicePolicyService.onCreate(); + } else if (DBG) { + Slogf.d(TAG, "Not setting PerUserCarDevicePolicyService because device doesn't have %s", + PackageManager.FEATURE_DEVICE_ADMIN); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mPerUserCarDevicePolicyService != null) { + mPerUserCarDevicePolicyService.onDestroy(); + } + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + super.dump(fd, writer, args); + try (IndentingPrintWriter pw = new IndentingPrintWriter(writer)) { + if (mPerUserCarDevicePolicyService != null) { + pw.println("PerUserCarDevicePolicyService"); + pw.increaseIndent(); + mPerUserCarDevicePolicyService.dump(pw); + pw.decreaseIndent(); + } else { + pw.println("PerUserCarDevicePolicyService not needed"); + } + pw.println(); + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + // keep it alive. + return START_STICKY; + } +} diff --git a/service-builtin/src/com/android/car/ServiceProxy.java b/service-builtin/src/com/android/car/ServiceProxy.java new file mode 100644 index 0000000000..549e052277 --- /dev/null +++ b/service-builtin/src/com/android/car/ServiceProxy.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; + +import static com.android.car.UpdatablePackageDependency.PROXIED_SERVICE_DO_ATTACH_BASE_CONTEXT; +import static com.android.car.UpdatablePackageDependency.PROXIED_SERVICE_DO_DUMP; +import static com.android.car.UpdatablePackageDependency.PROXIED_SERVICE_SET_BUILTIN_PACKAGE_CONTEXT; + +import android.annotation.Nullable; +import android.app.Service; +import android.car.builtin.util.Slog; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +/** + * Base class to wrap Service lifecycle with real Service code loaded from updatable car service + * package. Public Service defined inside builtin should inherit this to provide automatic + * wrapping. + */ +public class ServiceProxy extends Service { + private static final String TAG = "CAR.ServiceProxy"; + + private final String mRealServiceClassName; + + private UpdatablePackageContext mUpdatablePackageContext; + private Class mRealServiceClass; + private Service mRealService; + + public ServiceProxy(String realServiceClassName) { + mRealServiceClassName = realServiceClassName; + } + + @Override + public void onCreate() { + init(); + mRealService.onCreate(); + } + + @Override + public void onDestroy() { + mRealService.onDestroy(); + } + + @Override + public IBinder onBind(Intent intent) { + return mRealService.onBind(intent); + } + + @Override + public boolean onUnbind(Intent intent) { + return mRealService.onUnbind(intent); + } + @Override + public void onRebind(Intent intent) { + mRealService.onRebind(intent); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + return mRealService.onStartCommand(intent, flags, startId); + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + executeAMethod(PROXIED_SERVICE_DO_DUMP, + new Class[]{FileDescriptor.class, PrintWriter.class, args.getClass()}, + new Object[]{fd, writer, args}, true); + } + + private void init() { + mUpdatablePackageContext = UpdatablePackageContext.create(this); + try { + mRealServiceClass = mUpdatablePackageContext.getClassLoader().loadClass( + mRealServiceClassName); + // Use default constructor always + Constructor constructor = mRealServiceClass.getConstructor(); + mRealService = (Service) constructor.newInstance(); + executeAMethod(PROXIED_SERVICE_DO_ATTACH_BASE_CONTEXT, new Class[]{Context.class}, + new Object[]{mUpdatablePackageContext}, false); + executeAMethod(PROXIED_SERVICE_SET_BUILTIN_PACKAGE_CONTEXT, new Class[]{Context.class}, + new Object[]{this}, false); + } catch (Exception e) { + throw new RuntimeException("Cannot load class:" + mRealServiceClassName, e); + } + } + + /** Reflecion helper */ + @Nullable + public Object executeAMethod(String methodName, Class[] argClasses, Object[] args, + boolean ignoreFailure) { + try { + Method m = mRealServiceClass.getMethod(methodName, argClasses); + return m.invoke(mRealService, args); + } catch (Exception e) { + String msg = "cannot load method:" + methodName + " for:" + mRealServiceClassName; + if (ignoreFailure) { + Slog.w(TAG, msg, e); + return null; + } else { + throw new RuntimeException(msg, e); + } + } + } + + /** Check {@link Service#attachBaseContext(Context)}. */ + public void doAttachBaseContext(Context newBase) { + attachBaseContext(newBase); + } + + /** Check {@link Service#dump(FileDescriptor, PrintWriter, String[])}. */ + public void doDump(FileDescriptor fd, PrintWriter writer, String[] args) { + dump(fd, writer, args); + } +} diff --git a/service-builtin/src/com/android/car/UpdatablePackageContext.java b/service-builtin/src/com/android/car/UpdatablePackageContext.java new file mode 100644 index 0000000000..1f87891d9c --- /dev/null +++ b/service-builtin/src/com/android/car/UpdatablePackageContext.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.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; + +import android.content.Context; +import android.content.ContextWrapper; +import android.content.pm.PackageInfo; +import android.content.res.AssetManager; +import android.content.res.Resources; + +/** Context for updatable package */ +public class UpdatablePackageContext extends ContextWrapper { + + public static final String UPDATABLE_CAR_SERVICE_PACKAGE_NAME = "com.android.car.updatable"; + + // This is the package context of the com.android.car.updatable + private final Context mPackageContext; + + /** Create context for updatable package */ + public static UpdatablePackageContext create(Context baseContext) { + Context packageContext; + try { + PackageInfo info = baseContext.getPackageManager().getPackageInfo( + UPDATABLE_CAR_SERVICE_PACKAGE_NAME, 0); + if (info == null || info.applicationInfo == null || !(info.applicationInfo.isSystemApp() + || info.applicationInfo.isUpdatedSystemApp())) { + throw new IllegalStateException( + "Updated car service package is not usable:" + ((info == null) + ? "do not exist" : info.applicationInfo)); + } + // CONTEXT_IGNORE_SECURITY: UID is different but ok as the package is trustable system + // app + packageContext = baseContext.createPackageContext( + UPDATABLE_CAR_SERVICE_PACKAGE_NAME, + Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY); + } catch (Exception e) { + throw new RuntimeException("Cannot load updatable package code", e); + } + + return new UpdatablePackageContext(baseContext, packageContext); + } + + private UpdatablePackageContext(Context baseContext, Context packageContext) { + super(baseContext); + mPackageContext = packageContext; + } + + @Override + public AssetManager getAssets() { + return mPackageContext.getAssets(); + } + + @Override + public Resources getResources() { + return mPackageContext.getResources(); + } + + @Override + public ClassLoader getClassLoader() { + // This context cannot load code from builtin any more. + return mPackageContext.getClassLoader(); + } +} diff --git a/service-builtin/src/com/android/car/UpdatablePackageDependency.java b/service-builtin/src/com/android/car/UpdatablePackageDependency.java new file mode 100644 index 0000000000..f26ffb1033 --- /dev/null +++ b/service-builtin/src/com/android/car/UpdatablePackageDependency.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.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; + +/** + * Declared all dependency into updatable package, mostly for class / method names. + * + * <p> This is for tracking all dependencies done through java reflection. + */ +public class UpdatablePackageDependency { + private UpdatablePackageDependency() {} + + /** {@code com.android.car.ProxiedService#doAttachBaseContext()} method */ + public static final String PROXIED_SERVICE_DO_ATTACH_BASE_CONTEXT = "doAttachBaseContext"; + + /** {@code com.android.car.ProxiedService#setBuiltinPackageContext()} method */ + public static final String PROXIED_SERVICE_SET_BUILTIN_PACKAGE_CONTEXT = + "setBuiltinPackageContext"; + + /** {@code com.android.car.ProxiedService#doDump()} method */ + public static final String PROXIED_SERVICE_DO_DUMP = "doDump"; + + /** {@code com.android.car.CarServiceImpl} class */ + public static final String CAR_SERVICE_IMPL_CLASS = "com.android.car.CarServiceImpl"; + + /** {@code com.android.car.PerUserCarServiceImpl} class */ + public static final String PER_USER_CAR_SERVICE_IMPL_CLASS = + "com.android.car.PerUserCarServiceImpl"; + + /** {@code com.android.car.pm.CarSafetyAccessibilityServiceImpl} class */ + public static final String CAR_ACCESSIBILITY_IMPL_CLASS = + "com.android.car.pm.CarSafetyAccessibilityServiceImpl"; + + /** + * {@code com.android.car.pm.CarSafetyAccessibilityServiceImpl#onAccessibilityEvent()} method + */ + public static final String CAR_ACCESSIBILITY_ON_ACCESSIBILITY_EVENT = "onAccessibilityEvent"; +} diff --git a/service-builtin/src/com/android/car/admin/FactoryResetActivity.java b/service-builtin/src/com/android/car/admin/FactoryResetActivity.java new file mode 100644 index 0000000000..61640b3c6c --- /dev/null +++ b/service-builtin/src/com/android/car/admin/FactoryResetActivity.java @@ -0,0 +1,190 @@ +/* + * 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.android.car.admin; + +import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_PARKED; + +import static com.android.car.admin.NotificationHelper.FACTORY_RESET_NOTIFICATION_ID; + +import android.app.Activity; +import android.app.ActivityManager; +import android.app.AlertDialog; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.car.Car; +import android.car.builtin.util.Slog; +import android.car.drivingstate.CarDrivingStateEvent; +import android.car.drivingstate.CarDrivingStateManager; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; +import android.os.UserHandle; +import android.widget.Button; +import android.widget.Toast; + +import com.android.car.CarLog; +import com.android.car.R; +import com.android.internal.os.IResultReceiver; + +// TODO(b/171603586): STOPSHIP move this class to CarSettings +/** + * Activity shown when a factory request is imminent, it gives the user the option to reset now or + * wait until the device is rebooted / resumed from suspend. + */ +public final class FactoryResetActivity extends Activity { + + private static final String TAG = CarLog.tagFor(FactoryResetActivity.class); + + public static final String EXTRA_CALLBACK = "factory_reset_callback"; + + private Button mNowButton; + private Button mLaterButton; + private IResultReceiver mCallback; + + private Car mCar; + private CarDrivingStateManager mCarDrivingStateManager; + /** + * Sends the notification warning the user about the factory reset. + */ + public static void sendNotification(Context context, IResultReceiver callback) { + // The factory request is received by CarService - which runs on system user - but the + // notification will be sent to all users. + UserHandle currentUser = UserHandle.of(ActivityManager.getCurrentUser()); + + @SuppressWarnings("deprecation") + Intent intent = new Intent(context, FactoryResetActivity.class) + .putExtra(EXTRA_CALLBACK, callback.asBinder()); + PendingIntent pendingIntent = PendingIntent.getActivityAsUser(context, + FACTORY_RESET_NOTIFICATION_ID, intent, PendingIntent.FLAG_IMMUTABLE, + /* options= */ null, currentUser); + + // TODO (b/13679261) allows OEM to customize the package name shown in notification + Notification notification = NotificationHelper + .newNotificationBuilder(context, NotificationManager.IMPORTANCE_HIGH) + .setSmallIcon(R.drawable.car_ic_warning) + .setColor(context.getColor(R.color.red_warning)) + .setContentTitle(context.getString(R.string.factory_reset_notification_title)) + .setContentText(context.getString(R.string.factory_reset_notification_text)) + .setCategory(Notification.CATEGORY_CAR_WARNING) + .setOngoing(true) + .addAction(/* icon= */ 0, + context.getString(R.string.factory_reset_notification_button), + pendingIntent) + .build(); + + Slog.i(TAG, "Showing factory reset notification on all users"); + context.getSystemService(NotificationManager.class) + .notifyAsUser(TAG, FACTORY_RESET_NOTIFICATION_ID, notification, UserHandle.ALL); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + Object binder = null; + + try { + binder = intent.getExtra(EXTRA_CALLBACK); + mCallback = IResultReceiver.Stub.asInterface((IBinder) binder); + } catch (Exception e) { + Slog.w(TAG, "error getting IResultReveiver from " + EXTRA_CALLBACK + " extra (" + + binder + ") on " + intent, e); + } + + if (mCallback == null) { + Slog.wtf(TAG, "no IResultReceiver / " + EXTRA_CALLBACK + " extra on " + intent); + finish(); + return; + } + + // Connect to car service + mCar = Car.createCar(this); + mCarDrivingStateManager = (CarDrivingStateManager) mCar.getCarManager( + Car.CAR_DRIVING_STATE_SERVICE); + showMore(); + } + + @Override + protected void onStop() { + super.onStop(); + finish(); + } + + private void showMore() { + CarDrivingStateEvent state = mCarDrivingStateManager.getCurrentCarDrivingState(); + switch (state.eventValue) { + case DRIVING_STATE_PARKED: + showFactoryResetDialog(); + break; + default: + showFactoryResetToast(); + } + } + + private void showFactoryResetDialog() { + // TODO(b/171603586): STOPSHIP use Chassis library after moving this class to CarSettings + AlertDialog dialog = new AlertDialog.Builder(/* context= */ this, + com.android.internal.R.style.Theme_DeviceDefault_Dialog_Alert) + .setTitle(R.string.factory_reset_parked_title) + .setMessage(R.string.factory_reset_parked_text) + .setPositiveButton(R.string.factory_reset_later_button, + (d, which) -> factoryResetLater()) + .setNegativeButton(R.string.factory_reset_now_button, + (d, which) -> factoryResetNow()) + .setCancelable(false) + .setOnDismissListener((d) -> finish()) + .create(); + + dialog.show(); + } + + private void showFactoryResetToast() { + showToast(R.string.factory_reset_driving_text); + finish(); + } + + private void factoryResetNow() { + Slog.i(TAG, "Factory reset acknowledged; finishing it"); + + try { + mCallback.send(/* resultCode= */ 0, /* resultData= */ null); + + // Cancel pending intent and notification + getSystemService(NotificationManager.class).cancel(FACTORY_RESET_NOTIFICATION_ID); + PendingIntent.getActivity(this, FACTORY_RESET_NOTIFICATION_ID, getIntent(), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT).cancel(); + } catch (Exception e) { + Slog.e(TAG, "error factory resetting or cancelling notification / intent", e); + return; + } finally { + finish(); + } + } + + private void factoryResetLater() { + Slog.i(TAG, "Delaying factory reset."); + showToast(R.string.factory_reset_later_text); + finish(); + } + + private void showToast(int resId) { + Toast.makeText(this, resId, Toast.LENGTH_LONG).show(); + } +} diff --git a/service-builtin/src/com/android/car/admin/NewUserDisclaimerActivity.java b/service-builtin/src/com/android/car/admin/NewUserDisclaimerActivity.java new file mode 100644 index 0000000000..39cb9fe460 --- /dev/null +++ b/service-builtin/src/com/android/car/admin/NewUserDisclaimerActivity.java @@ -0,0 +1,115 @@ +/* + * 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.android.car.admin; + +import android.app.Activity; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.car.builtin.util.Slog; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.widget.Button; + +import com.android.car.CarLog; +import com.android.car.R; +import com.android.car.admin.ui.ManagedDeviceTextView; +import com.android.internal.annotations.VisibleForTesting; + +// TODO(b/171603586): STOPSHIP move UI related activities to CarSettings +/** + * Shows a disclaimer when a new user is added in a device that is managed by a device owner. + */ +public final class NewUserDisclaimerActivity extends Activity { + private static final boolean DEBUG = false; + private static final String TAG = CarLog.tagFor(NewUserDisclaimerActivity.class); + private static final int NOTIFICATION_ID = + NotificationHelper.NEW_USER_DISCLAIMER_NOTIFICATION_ID; + + private Button mAcceptButton; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.new_user_disclaimer); + + mAcceptButton = findViewById(R.id.accept_button); + mAcceptButton.setOnClickListener((v) -> accept()); + } + + @Override + protected void onResume() { + super.onResume(); + + if (DEBUG) Slog.d(TAG, "showing UI"); + + PerUserCarDevicePolicyService.getInstance(this).setShown(); + + // TODO(b/175057848): automotically finish the activity at x ms if the user doesn't ack it + // and/or integrate it with UserNoticeService + } + + @VisibleForTesting + Button getAcceptButton() { + return mAcceptButton; + } + + private void accept() { + if (DEBUG) Slog.d(TAG, "user accepted"); + + PerUserCarDevicePolicyService.getInstance(this).setAcknowledged(); + finish(); + } + + static void showNotification(Context context) { + PendingIntent pendingIntent = getPendingIntent(context, /* extraFlags= */ 0); + + Notification notification = NotificationHelper + .newNotificationBuilder(context, NotificationManager.IMPORTANCE_DEFAULT) + // TODO(b/175057848): proper icon? + .setSmallIcon(R.drawable.car_ic_mode) + .setContentTitle(context.getString(R.string.new_user_managed_notification_title)) + .setContentText(ManagedDeviceTextView.getManagedDeviceText(context)) + .setCategory(Notification.CATEGORY_CAR_INFORMATION) + .setContentIntent(pendingIntent) + .setOngoing(true) + .build(); + + if (DEBUG) { + Slog.d(TAG, "Showing new managed notification (id " + NOTIFICATION_ID + " on user " + + context.getUserId()); + } + context.getSystemService(NotificationManager.class).notify(NOTIFICATION_ID, notification); + } + + static void cancelNotification(Context context) { + if (DEBUG) { + Slog.d(TAG, "Canceling notification " + NOTIFICATION_ID + " for user " + + context.getUserId()); + } + context.getSystemService(NotificationManager.class).cancel(NOTIFICATION_ID); + getPendingIntent(context, PendingIntent.FLAG_UPDATE_CURRENT).cancel(); + } + + @VisibleForTesting + static PendingIntent getPendingIntent(Context context, int extraFlags) { + return PendingIntent.getActivity(context, NOTIFICATION_ID, + new Intent(context, NewUserDisclaimerActivity.class), + PendingIntent.FLAG_IMMUTABLE | extraFlags); + } +} diff --git a/service-builtin/src/com/android/car/admin/NotificationHelper.java b/service-builtin/src/com/android/car/admin/NotificationHelper.java new file mode 100644 index 0000000000..57f3d7ed78 --- /dev/null +++ b/service-builtin/src/com/android/car/admin/NotificationHelper.java @@ -0,0 +1,91 @@ +/* + * 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.android.car.admin; + +import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE; + +import android.annotation.NonNull; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.os.Bundle; + +import com.android.car.R; +import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; +import com.android.internal.annotations.VisibleForTesting; + +import java.util.Objects; + +// TODO: move this class to CarSettings or at least to some common package (not admin) +/** + * Helper for notification-related tasks + */ +public final class NotificationHelper { + + static final int FACTORY_RESET_NOTIFICATION_ID = 42; + + static final int NEW_USER_DISCLAIMER_NOTIFICATION_ID = 108; + + @VisibleForTesting + static final String CHANNEL_ID_DEFAULT = "channel_id_default"; + @VisibleForTesting + static final String CHANNEL_ID_HIGH = "channel_id_high"; + + /** + * Creates a notification (and its notification channel) for the given importance type, setting + * its name to be {@code Android System}. + * + * @param context context for showing the notification + * @param importance notification importance. Currently only + * {@link NotificationManager.IMPORTANCE_HIGH} is supported. + */ + @NonNull + public static Notification.Builder newNotificationBuilder(Context context, + @NotificationManager.Importance int importance) { + Objects.requireNonNull(context, "context cannot be null"); + + String channelId, importanceName; + switch (importance) { + case NotificationManager.IMPORTANCE_DEFAULT: + channelId = CHANNEL_ID_DEFAULT; + importanceName = context.getString(R.string.importance_default); + break; + case NotificationManager.IMPORTANCE_HIGH: + channelId = CHANNEL_ID_HIGH; + importanceName = context.getString(R.string.importance_high); + break; + default: + throw new IllegalArgumentException("Unsupported importance: " + importance); + } + NotificationManager notificationMgr = context.getSystemService(NotificationManager.class); + notificationMgr.createNotificationChannel( + new NotificationChannel(channelId, importanceName, importance)); + + Bundle extras = new Bundle(); + extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, + context.getString(com.android.internal.R.string.android_system_label)); + + return new Notification.Builder(context, channelId).addExtras(extras); + } + + @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE, + details = "private constructor") + private NotificationHelper() { + throw new UnsupportedOperationException("Contains only static methods"); + } +} diff --git a/service-builtin/src/com/android/car/admin/PerUserCarDevicePolicyService.java b/service-builtin/src/com/android/car/admin/PerUserCarDevicePolicyService.java new file mode 100644 index 0000000000..704d742167 --- /dev/null +++ b/service-builtin/src/com/android/car/admin/PerUserCarDevicePolicyService.java @@ -0,0 +1,189 @@ +/* + * 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.android.car.admin; + +import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; + +import android.annotation.IntDef; +import android.app.admin.DevicePolicyManager; +import android.car.builtin.util.Slog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.DebugUtils; +import android.util.IndentingPrintWriter; + +import com.android.car.CarLog; +import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** + * User-specific {@code CarDevicePolicyManagerService}. + */ +public final class PerUserCarDevicePolicyService { + + private static final String TAG = CarLog.tagFor(PerUserCarDevicePolicyService.class); + private static final boolean DEBUG = false; + + private static final String PREFIX_NEW_USER_DISCLAIMER_STATUS = "NEW_USER_DISCLAIMER_STATUS_"; + + // TODO(b/175057848) must be public because of DebugUtils.constantToString() + public static final int NEW_USER_DISCLAIMER_STATUS_NEVER_RECEIVED = 0; + public static final int NEW_USER_DISCLAIMER_STATUS_RECEIVED = 1; + public static final int NEW_USER_DISCLAIMER_STATUS_NOTIFICATION_SENT = 2; + public static final int NEW_USER_DISCLAIMER_STATUS_SHOWN = 3; + public static final int NEW_USER_DISCLAIMER_STATUS_ACKED = 4; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = false, prefix = { PREFIX_NEW_USER_DISCLAIMER_STATUS }, value = { + NEW_USER_DISCLAIMER_STATUS_NEVER_RECEIVED, + NEW_USER_DISCLAIMER_STATUS_NOTIFICATION_SENT, + NEW_USER_DISCLAIMER_STATUS_RECEIVED, + NEW_USER_DISCLAIMER_STATUS_SHOWN, + NEW_USER_DISCLAIMER_STATUS_ACKED + }) + public @interface NewUserDisclaimerStatus {} + + private static final Object SLOCK = new Object(); + + @GuardedBy("SLOCK") + private static PerUserCarDevicePolicyService sInstance; + + private final Context mContext; + + @GuardedBy("sLock") + @NewUserDisclaimerStatus + private int mNewUserDisclaimerStatus = NEW_USER_DISCLAIMER_STATUS_NEVER_RECEIVED; + + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (DEBUG) { + Slog.d(TAG, "Received intent on user " + mContext.getUserId() + ": " + intent); + } + switch(intent.getAction()) { + case DevicePolicyManager.ACTION_SHOW_NEW_USER_DISCLAIMER: + setUserDisclaimerStatus(NEW_USER_DISCLAIMER_STATUS_RECEIVED); + showNewUserDisclaimer(); + break; + default: + Slog.w(TAG, "received unexpected intent: " + intent); + } + } + }; + + /** + * Gests the singleton instance, creating it if necessary. + */ + public static PerUserCarDevicePolicyService getInstance(Context context) { + Objects.requireNonNull(context, "context cannot be null"); + + synchronized (SLOCK) { + if (sInstance == null) { + sInstance = new PerUserCarDevicePolicyService(context.getApplicationContext()); + if (DEBUG) Slog.d(TAG, "Created instance: " + sInstance); + } + + return sInstance; + } + } + + @VisibleForTesting + PerUserCarDevicePolicyService(Context context) { + mContext = context; + } + + /** + * Callback for when the service is created. + */ + public void onCreate() { + if (DEBUG) Slog.d(TAG, "registering BroadcastReceiver"); + + mContext.registerReceiver(mBroadcastReceiver, new IntentFilter( + DevicePolicyManager.ACTION_SHOW_NEW_USER_DISCLAIMER)); + } + + /** + * Callback for when the service is not needed anymore. + */ + public void onDestroy() { + synchronized (SLOCK) { + sInstance = null; + } + if (DEBUG) Slog.d(TAG, "unregistering BroadcastReceiver"); + mContext.unregisterReceiver(mBroadcastReceiver); + } + + /** + * Dump its contents. + */ + @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) + public void dump(IndentingPrintWriter pw) { + synchronized (SLOCK) { + pw.printf("mNewUserDisclaimerStatus: %s\n", + newUserDisclaimerStatusToString(mNewUserDisclaimerStatus)); + } + } + + void setShown() { + setUserDisclaimerStatus(NEW_USER_DISCLAIMER_STATUS_SHOWN); + } + + void setAcknowledged() { + setUserDisclaimerStatus(NEW_USER_DISCLAIMER_STATUS_ACKED); + NewUserDisclaimerActivity.cancelNotification(mContext); + + DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class); + dpm.resetNewUserDisclaimer(); + } + + private void showNewUserDisclaimer() { + // TODO(b/175057848) persist status so it's shown again if car service crashes? + NewUserDisclaimerActivity.showNotification(mContext); + setUserDisclaimerStatus(NEW_USER_DISCLAIMER_STATUS_NOTIFICATION_SENT); + } + + private void setUserDisclaimerStatus(@NewUserDisclaimerStatus int status) { + synchronized (SLOCK) { + if (DEBUG) { + Slog.d(TAG, "Changinging status from " + + newUserDisclaimerStatusToString(mNewUserDisclaimerStatus) + " to " + + newUserDisclaimerStatusToString(status)); + } + mNewUserDisclaimerStatus = status; + } + } + + @VisibleForTesting + @NewUserDisclaimerStatus + int getNewUserDisclaimerStatus() { + synchronized (SLOCK) { + return mNewUserDisclaimerStatus; + } + } + + @VisibleForTesting + static String newUserDisclaimerStatusToString(@NewUserDisclaimerStatus int status) { + return DebugUtils.constantToString(PerUserCarDevicePolicyService.class, + PREFIX_NEW_USER_DISCLAIMER_STATUS, status); + } +} diff --git a/service-builtin/src/com/android/car/am/ContinuousBlankActivity.java b/service-builtin/src/com/android/car/am/ContinuousBlankActivity.java new file mode 100644 index 0000000000..9a4817c284 --- /dev/null +++ b/service-builtin/src/com/android/car/am/ContinuousBlankActivity.java @@ -0,0 +1,42 @@ +/* + * 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.android.car.am; + +import android.app.Activity; +import android.car.builtin.util.Slog; +import android.os.Bundle; + +import com.android.car.CarLog; +import com.android.car.R; + +/** + * Activity to block top activity after suspend to RAM in case of guest user. + * + * <p> Guest user resuming will cause user switch to another guest user. + * For better user experience, no screen from previous user should be displayed. + */ + +public class ContinuousBlankActivity extends Activity { + private static final String TAG = CarLog.tagFor(ContinuousBlankActivity.class); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_continuous_blank); + Slog.i(TAG, "ContinuousBlankActivity created:"); + } +} diff --git a/service-builtin/src/com/android/car/pm/ActivityBlockingActivity.java b/service-builtin/src/com/android/car/pm/ActivityBlockingActivity.java new file mode 100644 index 0000000000..9d148e160b --- /dev/null +++ b/service-builtin/src/com/android/car/pm/ActivityBlockingActivity.java @@ -0,0 +1,432 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.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.pm; + +import static android.car.content.pm.CarPackageManager.BLOCKING_INTENT_EXTRA_BLOCKED_ACTIVITY_NAME; +import static android.car.content.pm.CarPackageManager.BLOCKING_INTENT_EXTRA_BLOCKED_TASK_ID; +import static android.car.content.pm.CarPackageManager.BLOCKING_INTENT_EXTRA_IS_ROOT_ACTIVITY_DO; +import static android.car.content.pm.CarPackageManager.BLOCKING_INTENT_EXTRA_ROOT_ACTIVITY_NAME; + +import android.app.Activity; +import android.app.ActivityManager; +import android.app.ActivityTaskManager.RootTaskInfo; +import android.app.IActivityManager; +import android.car.Car; +import android.car.builtin.util.Slog; +import android.car.content.pm.CarPackageManager; +import android.car.drivingstate.CarUxRestrictions; +import android.car.drivingstate.CarUxRestrictionsManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Insets; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.hardware.display.DisplayManager; +import android.opengl.GLSurfaceView; +import android.os.Build; +import android.os.Bundle; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.Log; +import android.view.Display; +import android.view.DisplayInfo; +import android.view.View; +import android.view.ViewTreeObserver; +import android.view.WindowInsets; +import android.widget.Button; +import android.widget.TextView; + +import com.android.car.CarLog; +import com.android.car.R; +import com.android.car.pm.blurredbackground.BlurredSurfaceRenderer; + +import java.util.List; + +/** + * Default activity that will be launched when the current foreground activity is not allowed. + * Additional information on blocked Activity should be passed as intent extras. + */ +public class ActivityBlockingActivity extends Activity { + private static final int EGL_CONTEXT_VERSION = 3; + private static final int EGL_CONFIG_SIZE = 8; + private static final int INVALID_TASK_ID = -1; + private final Object mLock = new Object(); + + private GLSurfaceView mGLSurfaceView; + private BlurredSurfaceRenderer mSurfaceRenderer; + private boolean mIsGLSurfaceSetup = false; + + private Car mCar; + private CarUxRestrictionsManager mUxRManager; + private CarPackageManager mCarPackageManager; + + private Button mExitButton; + private Button mToggleDebug; + + private int mBlockedTaskId; + private IActivityManager mAm; + + private final View.OnClickListener mOnExitButtonClickedListener = + v -> { + if (isExitOptionCloseApplication()) { + handleCloseApplication(); + } else { + handleRestartingTask(); + } + }; + + private final ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener = + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + mToggleDebug.getViewTreeObserver().removeOnGlobalLayoutListener(this); + updateButtonWidths(); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_blocking); + + mExitButton = findViewById(R.id.exit_button); + mAm = ActivityManager.getService(); + + // Listen to the CarUxRestrictions so this blocking activity can be dismissed when the + // restrictions are lifted. + // This Activity should be launched only after car service is initialized. Currently this + // Activity is only launched from CPMS. So this is safe to do. + mCar = Car.createCar(this, /* handler= */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, + (car, ready) -> { + if (!ready) { + return; + } + mCarPackageManager = (CarPackageManager) car.getCarManager( + Car.PACKAGE_SERVICE); + mUxRManager = (CarUxRestrictionsManager) car.getCarManager( + Car.CAR_UX_RESTRICTION_SERVICE); + // This activity would have been launched only in a restricted state. + // But ensuring when the service connection is established, that we are still + // in a restricted state. + handleUxRChange(mUxRManager.getCurrentCarUxRestrictions()); + mUxRManager.registerListener(ActivityBlockingActivity.this::handleUxRChange); + }); + + setupGLSurface(); + } + + @Override + protected void onStart() { + super.onStart(); + if (mIsGLSurfaceSetup) { + mGLSurfaceView.onResume(); + } + } + + @Override + protected void onResume() { + super.onResume(); + + // Display info about the current blocked activity, and optionally show an exit button + // to restart the blocked task (stack of activities) if its root activity is DO. + mBlockedTaskId = getIntent().getIntExtra(BLOCKING_INTENT_EXTRA_BLOCKED_TASK_ID, + INVALID_TASK_ID); + + // blockedActivity is expected to be always passed in as the topmost activity of task. + String blockedActivity = getIntent().getStringExtra( + BLOCKING_INTENT_EXTRA_BLOCKED_ACTIVITY_NAME); + if (!TextUtils.isEmpty(blockedActivity)) { + if (isTopActivityBehindAbaDistractionOptimized()) { + Slog.e(CarLog.TAG_AM, "Top activity is already DO, so finishing"); + finish(); + return; + } + + if (Log.isLoggable(CarLog.TAG_AM, Log.DEBUG)) { + Slog.d(CarLog.TAG_AM, "Blocking activity " + blockedActivity); + } + } + + displayExitButton(); + + // Show more debug info for non-user build. + if (Build.IS_ENG || Build.IS_USERDEBUG) { + displayDebugInfo(); + } + } + + @Override + protected void onStop() { + super.onStop(); + + if (mIsGLSurfaceSetup) { + // We queue this event so that it runs on the Rendering thread + mGLSurfaceView.queueEvent(() -> mSurfaceRenderer.onPause()); + + mGLSurfaceView.onPause(); + } + + // Finish when blocking activity goes invisible to avoid it accidentally re-surfaces with + // stale string regarding blocked activity. + finish(); + } + + private void setupGLSurface() { + DisplayManager displayManager = (DisplayManager) getApplicationContext().getSystemService( + Context.DISPLAY_SERVICE); + DisplayInfo displayInfo = new DisplayInfo(); + + int displayId = getDisplayId(); + displayManager.getDisplay(displayId).getDisplayInfo(displayInfo); + + Rect windowRect = getAppWindowRect(); + + // We currently don't support blur for secondary display + // (because it is hard to take a screenshot of a secondary display) + // So for secondary displays, the GLSurfaceView will not appear blurred + boolean shouldRenderBlurred = getDisplayId() == Display.DEFAULT_DISPLAY; + + mSurfaceRenderer = new BlurredSurfaceRenderer(this, windowRect, shouldRenderBlurred); + + mGLSurfaceView = findViewById(R.id.blurred_surface_view); + mGLSurfaceView.setEGLContextClientVersion(EGL_CONTEXT_VERSION); + + // Sets up the surface so that we can make it translucent if needed + mGLSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT); + mGLSurfaceView.setEGLConfigChooser(EGL_CONFIG_SIZE, EGL_CONFIG_SIZE, EGL_CONFIG_SIZE, + EGL_CONFIG_SIZE, EGL_CONFIG_SIZE, EGL_CONFIG_SIZE); + + mGLSurfaceView.setRenderer(mSurfaceRenderer); + + // We only want to render the screen once + mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + + mIsGLSurfaceSetup = true; + } + + /** + * Computes a Rect that represents the portion of the screen that contains the activity that is + * being blocked. + * + * @return Rect that represents the application window + */ + private Rect getAppWindowRect() { + Insets systemBarInsets = getWindowManager() + .getCurrentWindowMetrics() + .getWindowInsets() + .getInsets(WindowInsets.Type.systemBars()); + + Rect displayBounds = getWindowManager().getCurrentWindowMetrics().getBounds(); + + int leftX = systemBarInsets.left; + int rightX = displayBounds.width() - systemBarInsets.right; + int topY = systemBarInsets.top; + int bottomY = displayBounds.height() - systemBarInsets.bottom; + + return new Rect(leftX, topY, rightX, bottomY); + } + + private void displayExitButton() { + String exitButtonText = getExitButtonText(); + + mExitButton.setText(exitButtonText); + mExitButton.setOnClickListener(mOnExitButtonClickedListener); + } + + // If the root activity is DO, the user will have the option to go back to that activity, + // otherwise, the user will have the option to close the blocked application + private boolean isExitOptionCloseApplication() { + boolean isRootDO = getIntent().getBooleanExtra( + BLOCKING_INTENT_EXTRA_IS_ROOT_ACTIVITY_DO, false); + return mBlockedTaskId == INVALID_TASK_ID || !isRootDO; + } + + private String getExitButtonText() { + return isExitOptionCloseApplication() ? getString(R.string.exit_button_close_application) + : getString(R.string.exit_button_go_back); + } + + /** + * It is possible that the stack info has changed between when the intent to launch this + * activity was initiated and when this activity is started. Check whether the activity behind + * the ABA is distraction optimized. + */ + private boolean isTopActivityBehindAbaDistractionOptimized() { + List<RootTaskInfo> taskInfos; + try { + taskInfos = mAm.getAllRootTaskInfos(); + } catch (RemoteException e) { + Slog.e(CarLog.TAG_AM, "Unable to get stack info from ActivityManager"); + // assume that the state is still correct, the activity behind is not DO + return false; + } + + RootTaskInfo topStackBehindAba = null; + for (RootTaskInfo taskInfo : taskInfos) { + if (taskInfo.displayId != getDisplayId()) { + // ignore stacks on other displays + continue; + } + + if (getComponentName().equals(taskInfo.topActivity)) { + // ignore stack with the blocking activity + continue; + } + + if (!taskInfo.visible) { + // ignore stacks that aren't visible + continue; + } + + if (topStackBehindAba == null || topStackBehindAba.position < taskInfo.position) { + topStackBehindAba = taskInfo; + } + } + + if (Log.isLoggable(CarLog.TAG_AM, Log.DEBUG)) { + Slog.d(CarLog.TAG_AM, String.format("Top stack behind ABA is: %s", topStackBehindAba)); + } + + if (topStackBehindAba != null && topStackBehindAba.topActivity != null) { + boolean isDo = mCarPackageManager.isActivityDistractionOptimized( + topStackBehindAba.topActivity.getPackageName(), + topStackBehindAba.topActivity.getClassName()); + if (Log.isLoggable(CarLog.TAG_AM, Log.DEBUG)) { + Slog.d(CarLog.TAG_AM, + String.format("Top activity (%s) is DO: %s", topStackBehindAba.topActivity, + isDo)); + } + return isDo; + } + + // unknown top stack / activity, default to considering it non-DO + return false; + } + + private void displayDebugInfo() { + String blockedActivity = getIntent().getStringExtra( + BLOCKING_INTENT_EXTRA_BLOCKED_ACTIVITY_NAME); + String rootActivity = getIntent().getStringExtra(BLOCKING_INTENT_EXTRA_ROOT_ACTIVITY_NAME); + + TextView debugInfo = findViewById(R.id.debug_info); + debugInfo.setText(getDebugInfo(blockedActivity, rootActivity)); + + // We still want to ensure driving safety for non-user build; + // toggle visibility of debug info with this button. + mToggleDebug = findViewById(R.id.toggle_debug_info); + mToggleDebug.setVisibility(View.VISIBLE); + mToggleDebug.setOnClickListener(v -> { + boolean isDebugVisible = debugInfo.getVisibility() == View.VISIBLE; + debugInfo.setVisibility(isDebugVisible ? View.GONE : View.VISIBLE); + }); + + mToggleDebug.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener); + } + + // When the Debug button is visible, we set both of the visible buttons to have the width + // of whichever button is wider + private void updateButtonWidths() { + Button debugButton = findViewById(R.id.toggle_debug_info); + + int exitButtonWidth = mExitButton.getWidth(); + int debugButtonWidth = debugButton.getWidth(); + + if (exitButtonWidth > debugButtonWidth) { + debugButton.setWidth(exitButtonWidth); + } else { + mExitButton.setWidth(debugButtonWidth); + } + } + + private String getDebugInfo(String blockedActivity, String rootActivity) { + StringBuilder debug = new StringBuilder(); + + ComponentName blocked = ComponentName.unflattenFromString(blockedActivity); + debug.append("Blocked activity is ") + .append(blocked.getShortClassName()) + .append("\nBlocked activity package is ") + .append(blocked.getPackageName()); + + if (rootActivity != null) { + ComponentName root = ComponentName.unflattenFromString(rootActivity); + // Optionally show root activity info if it differs from the blocked activity. + if (!root.equals(blocked)) { + debug.append("\n\nRoot activity is ").append(root.getShortClassName()); + } + if (!root.getPackageName().equals(blocked.getPackageName())) { + debug.append("\nRoot activity package is ").append(root.getPackageName()); + } + } + return debug.toString(); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mCar.disconnect(); + mUxRManager.unregisterListener(); + if (mToggleDebug != null) { + mToggleDebug.getViewTreeObserver().removeOnGlobalLayoutListener( + mOnGlobalLayoutListener); + } + mCar.disconnect(); + } + + // If no distraction optimization is required in the new restrictions, then dismiss the + // blocking activity (self). + private void handleUxRChange(CarUxRestrictions restrictions) { + if (restrictions == null) { + return; + } + if (!restrictions.isRequiresDistractionOptimization()) { + finish(); + } + } + + private void handleCloseApplication() { + if (isFinishing()) { + return; + } + + Intent startMain = new Intent(Intent.ACTION_MAIN); + startMain.addCategory(Intent.CATEGORY_HOME); + startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(startMain); + finish(); + } + + private void handleRestartingTask() { + // Lock on self to avoid restarting the same task twice. + synchronized (mLock) { + if (isFinishing()) { + return; + } + + if (Log.isLoggable(CarLog.TAG_AM, Log.INFO)) { + Slog.i(CarLog.TAG_AM, "Restarting task " + mBlockedTaskId); + } + mCarPackageManager.restartTask(mBlockedTaskId); + finish(); + } + } +} diff --git a/service-builtin/src/com/android/car/pm/CarSafetyAccessibilityService.java b/service-builtin/src/com/android/car/pm/CarSafetyAccessibilityService.java new file mode 100644 index 0000000000..321241b464 --- /dev/null +++ b/service-builtin/src/com/android/car/pm/CarSafetyAccessibilityService.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.pm; + +import android.accessibilityservice.AccessibilityService; +import android.view.accessibility.AccessibilityEvent; + +import com.android.car.ServiceProxy; +import com.android.car.UpdatablePackageContext; +import com.android.car.UpdatablePackageDependency; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +/** Proxy for CarSafetyAccessibilityServiceImpl */ +public class CarSafetyAccessibilityService extends AccessibilityService { + private final ServiceProxy mProxy = new ServiceProxy( + UpdatablePackageDependency.CAR_ACCESSIBILITY_IMPL_CLASS); + + private UpdatablePackageContext mUpdatablePackageContext; + private Object mImpl; + private Method mOnAccessibilityEventMethod; + + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + try { + mOnAccessibilityEventMethod.invoke(mImpl, new Object[]{event}); + } catch (Exception e) { + throw new RuntimeException("Cannot call onAccessibilityEvent", e); + } + } + + @Override + public void onInterrupt() { + } + + @Override + public void onCreate() { + UpdatablePackageContext updatablePackageContext = UpdatablePackageContext.create(this); + try { + Class implClass = updatablePackageContext.getClassLoader().loadClass( + UpdatablePackageDependency.CAR_ACCESSIBILITY_IMPL_CLASS); + // Use default constructor always + Constructor constructor = implClass.getConstructor(); + mImpl = constructor.newInstance(); + mOnAccessibilityEventMethod = implClass.getMethod( + UpdatablePackageDependency.CAR_ACCESSIBILITY_ON_ACCESSIBILITY_EVENT, + new Class[]{AccessibilityEvent.class}); + } catch (Exception e) { + throw new RuntimeException("Cannot load impl class", e); + } + } +} diff --git a/service-builtin/src/com/android/car/pm/blurredbackground/BlurTextureProgram.java b/service-builtin/src/com/android/car/pm/blurredbackground/BlurTextureProgram.java new file mode 100644 index 0000000000..f5ee937db1 --- /dev/null +++ b/service-builtin/src/com/android/car/pm/blurredbackground/BlurTextureProgram.java @@ -0,0 +1,337 @@ +/* + * 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.android.car.pm.blurredbackground; + +import android.graphics.Rect; +import android.opengl.GLES11Ext; +import android.opengl.GLES30; + +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +/** + * A class containing the OpenGL programs used to render a blurred texture + */ +public class BlurTextureProgram { + + private static final float[] FRAME_COORDS = { + -1.0f, -1.0f, // 0 bottom left + 1.0f, -1.0f, // 1 bottom right + -1.0f, 1.0f, // 2 top left + 1.0f, 1.0f, // 3 top right + }; + private static final float[] TEXTURE_COORDS = { + 0.0f, 0.0f, // 0 bottom left + 1.0f, 0.0f, // 1 bottom right + 0.0f, 1.0f, // 2 top left + 1.0f, 1.0f // 3 top right + }; + + private static final float[] INVERTED_TEXTURE_COORDS = { + 0.0f, 1.0f, // 0 bottom left + 1.0f, 1.0f, // 1 bottom right + 0.0f, 0.0f, // 2 top left + 1.0f, 0.0f // 3 top right + }; + + private static final int SIZEOF_FLOAT = 4; + private static final int NUM_COORDS_PER_VERTEX = 2; + private static final float BLUR_RADIUS = 40.0f; + + private final String mVertexShader; + private final String mHorizontalBlurShader; + private final String mVerticalBlurShader; + + private final int mHorizontalBlurProgram; + private final int mVerticalBlurProgram; + + private final int mWidth; + private final int mHeight; + + private final int mScreenshotTextureId; + private final IntBuffer mScreenshotTextureBuffer; + private final float[] mTexMatrix; + private final FloatBuffer mResolutionBuffer; + + private final FloatBuffer mVertexBuffer = GLHelper.createFloatBuffer(FRAME_COORDS); + private final FloatBuffer mTexBuffer = GLHelper.createFloatBuffer(TEXTURE_COORDS); + private final FloatBuffer mInvertedTexBuffer = GLHelper.createFloatBuffer( + INVERTED_TEXTURE_COORDS); + + // Locations of the uniforms and attributes for the horizontal program + private final int mUHorizontalMVPMatrixLoc; + private final int mUHorizontalTexMatrixLoc; + private final int mUHorizontalResolutionLoc; + private final int mUHorizontalRadiusLoc; + private final int mAHorizontalPositionLoc; + private final int mAHorizontalTextureCoordLoc; + + // Locations of the uniforms and attributes for the vertical program + private final int mUVerticalMVPMatrixLoc; + private final int mUVerticalTexMatrixLoc; + private final int mUVerticalResolutionLoc; + private final int mUVerticalRadiusLoc; + private final int mAVerticalPositionLoc; + private final int mAVerticalTextureCoordLoc; + + private final IntBuffer mFrameBuffer = IntBuffer.allocate(1); + private final IntBuffer mFirstPassTextureBuffer = IntBuffer.allocate(1); + + private int mFrameBufferId; + private int mFirstPassTextureId; + + /** + * Constructor for the BlurTextureProgram + * + * @param screenshotTextureBuffer IntBuffer + * @param texMatrix Float array used to scale the screenshot texture + * @param vertexShader String containing the horizontal blur shader + * @param horizontalBlurShader String containing the fragment shader for horizontal + * blur + * @param verticalBlurShader String containing the fragment shader for vertical blur + * @param windowRect Rect representing the location of the window being covered + */ + BlurTextureProgram( + IntBuffer screenshotTextureBuffer, + float[] texMatrix, + String vertexShader, + String horizontalBlurShader, + String verticalBlurShader, + Rect windowRect + ) { + mVertexShader = vertexShader; + mHorizontalBlurShader = horizontalBlurShader; + mVerticalBlurShader = verticalBlurShader; + + mScreenshotTextureBuffer = screenshotTextureBuffer; + mScreenshotTextureId = screenshotTextureBuffer.get(0); + mTexMatrix = texMatrix; + + mHorizontalBlurProgram = GLHelper.createProgram(mVertexShader, mHorizontalBlurShader); + mVerticalBlurProgram = GLHelper.createProgram(mVertexShader, mVerticalBlurShader); + + mWidth = windowRect.width(); + mHeight = windowRect.height(); + + mResolutionBuffer = FloatBuffer.wrap(new float[]{(float) mWidth, (float) mHeight, 1.0f}); + + // Initialize the uniform and attribute locations for the horizontal blur program + mUHorizontalMVPMatrixLoc = GLES30.glGetUniformLocation(mHorizontalBlurProgram, + "uMVPMatrix"); + mUHorizontalTexMatrixLoc = GLES30.glGetUniformLocation(mHorizontalBlurProgram, + "uTexMatrix"); + mUHorizontalResolutionLoc = GLES30.glGetUniformLocation(mHorizontalBlurProgram, + "uResolution"); + mUHorizontalRadiusLoc = GLES30.glGetUniformLocation(mHorizontalBlurProgram, "uRadius"); + + mAHorizontalPositionLoc = GLES30.glGetAttribLocation(mHorizontalBlurProgram, "aPosition"); + mAHorizontalTextureCoordLoc = GLES30.glGetAttribLocation(mHorizontalBlurProgram, + "aTextureCoord"); + + // Initialize the uniform and attribute locations for the vertical blur program + mUVerticalMVPMatrixLoc = GLES30.glGetUniformLocation(mVerticalBlurProgram, "uMVPMatrix"); + mUVerticalTexMatrixLoc = GLES30.glGetUniformLocation(mVerticalBlurProgram, "uTexMatrix"); + mUVerticalResolutionLoc = GLES30.glGetUniformLocation(mVerticalBlurProgram, "uResolution"); + mUVerticalRadiusLoc = GLES30.glGetUniformLocation(mVerticalBlurProgram, "uRadius"); + + mAVerticalPositionLoc = GLES30.glGetAttribLocation(mVerticalBlurProgram, "aPosition"); + mAVerticalTextureCoordLoc = GLES30.glGetAttribLocation(mVerticalBlurProgram, + "aTextureCoord"); + } + + /** + * Executes all of the rendering logic. Sets up FrameBuffers and programs to complete two + * rendering passes on the captured screenshot to produce a blur. + */ + public void render() { + setupProgram(mHorizontalBlurProgram, mScreenshotTextureId, + GLES11Ext.GL_TEXTURE_EXTERNAL_OES); + setHorizontalUniformsAndAttributes(); + + // Create the framebuffer that will hold the texture we render to + // for the first shader pass + mFrameBufferId = GLHelper.createAndBindFramebuffer(mFrameBuffer); + + // Create the empty texture that will store the output of the first shader pass (this'll + // be held in the Framebuffer Object) + mFirstPassTextureId = GLHelper.createAndBindTextureObject(mFirstPassTextureBuffer, + GLES30.GL_TEXTURE_2D); + + setupTextureForFramebuffer(mFirstPassTextureId); + assertValidFramebufferStatus(); + renderToFramebuffer(mFrameBufferId); + + setupProgram(mVerticalBlurProgram, mFirstPassTextureId, GLES30.GL_TEXTURE_2D); + setVerticalUniformsAndAttributes(); + + renderToSurface(); + cleanupResources(); + } + + /** + * Cleans up all OpenGL resources used by programs in this class + */ + public void cleanupResources() { + deleteFramebufferTexture(); + deleteFrameBuffer(); + deletePrograms(); + + GLES30.glFlush(); + } + + /** + * Attaches a 2D texture image to the active framebuffer object + * + * @param textureId The ID of the texture to be attached + */ + private void setupTextureForFramebuffer(int textureId) { + GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGB, mWidth, mHeight, 0, + GLES30.GL_RGB, GLES30.GL_UNSIGNED_BYTE, null); + GLHelper.checkGlErrors("glTexImage2D"); + GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, + GLES30.GL_TEXTURE_2D, textureId, 0); + GLHelper.checkGlErrors("glFramebufferTexture2D"); + } + + /** + * Deletes the texture stored in the framebuffer + */ + private void deleteFramebufferTexture() { + GLES30.glDeleteTextures(mFirstPassTextureBuffer.capacity(), mFirstPassTextureBuffer); + GLHelper.checkGlErrors("glDeleteTextures"); + } + + /** + * Deletes the frame buffers. + */ + private void deleteFrameBuffer() { + GLES30.glDeleteBuffers(1, mFrameBuffer); + GLHelper.checkGlErrors("glDeleteBuffers"); + } + + /** + * Deletes the GL programs. + */ + private void deletePrograms() { + GLES30.glDeleteProgram(mHorizontalBlurProgram); + GLHelper.checkGlErrors("glDeleteProgram"); + GLES30.glDeleteProgram(mVerticalBlurProgram); + GLHelper.checkGlErrors("glDeleteProgram"); + } + + /** + * Set all of the Uniform and Attribute variable values for the horizontal blur program + */ + private void setHorizontalUniformsAndAttributes() { + GLES30.glUniformMatrix4fv(mUHorizontalMVPMatrixLoc, 1, false, GLHelper.getIdentityMatrix(), + 0); + GLES30.glUniformMatrix4fv(mUHorizontalTexMatrixLoc, 1, false, mTexMatrix, 0); + GLES30.glUniform3fv(mUHorizontalResolutionLoc, 1, mResolutionBuffer); + GLES30.glUniform1f(mUHorizontalRadiusLoc, BLUR_RADIUS); + + GLES30.glEnableVertexAttribArray(mAHorizontalPositionLoc); + GLES30.glVertexAttribPointer(mAHorizontalPositionLoc, NUM_COORDS_PER_VERTEX, + GLES30.GL_FLOAT, false, NUM_COORDS_PER_VERTEX * SIZEOF_FLOAT, mVertexBuffer); + + GLES30.glEnableVertexAttribArray(mAHorizontalTextureCoordLoc); + GLES30.glVertexAttribPointer(mAHorizontalTextureCoordLoc, 2, + GLES30.GL_FLOAT, false, 2 * SIZEOF_FLOAT, mTexBuffer); + } + + /** + * Set all of the Uniform and Attribute variable values for the vertical blur program + */ + private void setVerticalUniformsAndAttributes() { + GLES30.glUniformMatrix4fv(mUVerticalMVPMatrixLoc, 1, false, GLHelper.getIdentityMatrix(), + 0); + GLES30.glUniformMatrix4fv(mUVerticalTexMatrixLoc, 1, false, mTexMatrix, 0); + GLES30.glUniform3fv(mUVerticalResolutionLoc, 1, mResolutionBuffer); + GLES30.glUniform1f(mUVerticalRadiusLoc, BLUR_RADIUS); + + GLES30.glEnableVertexAttribArray(mAVerticalPositionLoc); + GLES30.glVertexAttribPointer(mAVerticalPositionLoc, NUM_COORDS_PER_VERTEX, + GLES30.GL_FLOAT, false, NUM_COORDS_PER_VERTEX * SIZEOF_FLOAT, mVertexBuffer); + + GLES30.glEnableVertexAttribArray(mAVerticalTextureCoordLoc); + GLES30.glVertexAttribPointer(mAVerticalTextureCoordLoc, 2, + GLES30.GL_FLOAT, false, 2 * SIZEOF_FLOAT, mInvertedTexBuffer); + } + + /** + * Sets the program to be used in the next rendering, and binds a texture to it + * + * @param programId The Id of the program + * @param textureId The Id of the texture to be bound + * @param textureTarget The type of texture that is being bound + */ + private void setupProgram(int programId, int textureId, int textureTarget) { + GLES30.glUseProgram(programId); + GLHelper.checkGlErrors("glUseProgram"); + + GLES30.glActiveTexture(GLES30.GL_TEXTURE0); + GLHelper.checkGlErrors("glActiveTexture"); + + GLES30.glBindTexture(textureTarget, textureId); + GLHelper.checkGlErrors("glBindTexture"); + } + + /** + * Renders to a framebuffer using the current active program + * + * @param framebufferId The Id of the framebuffer being rendered to + */ + private void renderToFramebuffer(int framebufferId) { + GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); + GLHelper.checkGlErrors("glClear"); + + GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, framebufferId); + GLHelper.checkGlErrors("glBindFramebuffer"); + + GLES30.glViewport(0, 0, mWidth, mHeight); + GLHelper.checkGlErrors("glViewport"); + + GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, + FRAME_COORDS.length / NUM_COORDS_PER_VERTEX); + GLHelper.checkGlErrors("glDrawArrays"); + + GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, 0); + } + + /** + * Renders to a the GLSurface using the current active program + */ + private void renderToSurface() { + GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, 0); + GLHelper.checkGlErrors("glDrawArrays"); + + GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); + GLHelper.checkGlErrors("glDrawArrays"); + + GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, + FRAME_COORDS.length / NUM_COORDS_PER_VERTEX); + GLHelper.checkGlErrors("glDrawArrays"); + } + + private void assertValidFramebufferStatus() { + if (GLES30.glCheckFramebufferStatus(GLES30.GL_FRAMEBUFFER) + != GLES30.GL_FRAMEBUFFER_COMPLETE) { + throw new RuntimeException( + "Failed to attach Framebuffer. Framebuffer status code is: " + + GLES30.glCheckFramebufferStatus(GLES30.GL_FRAMEBUFFER)); + } + } +} diff --git a/service-builtin/src/com/android/car/pm/blurredbackground/BlurredSurfaceRenderer.java b/service-builtin/src/com/android/car/pm/blurredbackground/BlurredSurfaceRenderer.java new file mode 100644 index 0000000000..62d085e896 --- /dev/null +++ b/service-builtin/src/com/android/car/pm/blurredbackground/BlurredSurfaceRenderer.java @@ -0,0 +1,196 @@ +/* + * 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.android.car.pm.blurredbackground; + +import android.car.builtin.util.Slog; +import android.content.Context; +import android.graphics.Rect; +import android.graphics.SurfaceTexture; +import android.opengl.GLES11Ext; +import android.opengl.GLES30; +import android.opengl.GLSurfaceView; +import android.os.IBinder; +import android.view.Surface; +import android.view.SurfaceControl; + +import com.android.car.CarLog; +import com.android.car.R; + +import java.nio.IntBuffer; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +/** + * The renderer class for the {@link GLSurfaceView} of the {@link ActivityBlockingActivity} + */ +public class BlurredSurfaceRenderer implements GLSurfaceView.Renderer { + + private static final String TAG = CarLog.tagFor(BlurredSurfaceRenderer.class); + private static final int NUM_INDICES_TO_RENDER = 4; + + private final String mVertexShader; + private final String mHorizontalBlurShader; + private final String mVerticalBlurShader; + private final Rect mWindowRect; + + private BlurTextureProgram mProgram; + private SurfaceTexture mSurfaceTexture; + private Surface mSurface; + + private int mScreenshotTextureId; + private final IntBuffer mScreenshotTextureBuffer = IntBuffer.allocate(1); + private final float[] mTexMatrix = new float[16]; + + private final boolean mShadersLoadedSuccessfully; + private final boolean mShouldRenderBlurred; + private boolean mIsScreenShotCaptured = false; + + /** + * Constructs a new {@link BlurredSurfaceRenderer} and loads the shaders needed for rendering a + * blurred texture + * + * @param windowRect Rect that represents the application window + */ + public BlurredSurfaceRenderer(Context context, Rect windowRect, boolean shouldRenderBlurred) { + mShouldRenderBlurred = shouldRenderBlurred; + + mVertexShader = GLHelper.getShaderFromRaw(context, R.raw.vertex_shader); + mHorizontalBlurShader = GLHelper.getShaderFromRaw(context, + R.raw.horizontal_blur_fragment_shader); + mVerticalBlurShader = GLHelper.getShaderFromRaw(context, + R.raw.vertical_blur_fragment_shader); + + mShadersLoadedSuccessfully = mVertexShader != null + && mHorizontalBlurShader != null + && mVerticalBlurShader != null; + + mWindowRect = windowRect; + } + + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + mScreenshotTextureId = GLHelper.createAndBindTextureObject(mScreenshotTextureBuffer, + GLES11Ext.GL_TEXTURE_EXTERNAL_OES); + + mSurfaceTexture = new SurfaceTexture(mScreenshotTextureId); + mSurface = new Surface(mSurfaceTexture); + + if (mShouldRenderBlurred) { + mIsScreenShotCaptured = captureScreenshot(); + } + } + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + } + + @Override + public void onDrawFrame(GL10 gl) { + if (shouldDrawFrame()) { + mProgram = new BlurTextureProgram( + mScreenshotTextureBuffer, + mTexMatrix, + mVertexShader, + mHorizontalBlurShader, + mVerticalBlurShader, + mWindowRect + ); + mProgram.render(); + } else { + logWillNotRenderBlurredMsg(); + + // If we determine we shouldn't render a blurred texture, we + // will default to rendering a transparent GLSurfaceView so that + // the ActivityBlockingActivity appears translucent + renderTransparent(); + } + } + + private void renderTransparent() { + GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); + GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, /*first index to render */ 0, + NUM_INDICES_TO_RENDER); + } + + /** + * Called when the ActivityBlockingActivity pauses cleans up the OpenGL program + */ + public void onPause() { + if (mProgram != null) { + mProgram.cleanupResources(); + } + deleteScreenshotTexture(); + } + + private boolean captureScreenshot() { + boolean isScreenshotCaptured = false; + + try { + final IBinder token = SurfaceControl.getInternalDisplayToken(); + if (token == null) { + Slog.e(TAG, + "Could not find display token for screenshot. Will not capture screenshot"); + } else { + final SurfaceControl.DisplayCaptureArgs captureArgs = + new SurfaceControl.DisplayCaptureArgs.Builder(token) + .setSize(mWindowRect.width(), mWindowRect.height()) + .setSourceCrop(mWindowRect) + .setUseIdentityTransform(true) + .build(); + + SurfaceControl.ScreenshotHardwareBuffer screenshotHardwareBuffer = + SurfaceControl.captureDisplay(captureArgs); + mSurface.attachAndQueueBufferWithColorSpace( + screenshotHardwareBuffer.getHardwareBuffer(), + screenshotHardwareBuffer.getColorSpace()); + mSurfaceTexture.updateTexImage(); + mSurfaceTexture.getTransformMatrix(mTexMatrix); + isScreenshotCaptured = true; + } + + } finally { + mSurface.release(); + mSurfaceTexture.release(); + } + + return isScreenshotCaptured; + } + + private void deleteScreenshotTexture() { + GLES30.glDeleteTextures(mScreenshotTextureBuffer.capacity(), mScreenshotTextureBuffer); + GLHelper.checkGlErrors("glDeleteTextures"); + + mIsScreenShotCaptured = false; + } + + private void logWillNotRenderBlurredMsg() { + if (!mIsScreenShotCaptured) { + Slog.e(TAG, "Screenshot was not captured. Will not render blurred surface"); + } + if (!mShadersLoadedSuccessfully) { + Slog.e(TAG, "Shaders were not loaded successfully. Will not render blurred surface"); + } + } + + private boolean shouldDrawFrame() { + return mIsScreenShotCaptured + && mShadersLoadedSuccessfully + && mShouldRenderBlurred; + } +} + diff --git a/service-builtin/src/com/android/car/pm/blurredbackground/GLHelper.java b/service-builtin/src/com/android/car/pm/blurredbackground/GLHelper.java new file mode 100644 index 0000000000..e005bcc14e --- /dev/null +++ b/service-builtin/src/com/android/car/pm/blurredbackground/GLHelper.java @@ -0,0 +1,195 @@ +/* + * 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.android.car.pm.blurredbackground; + +import android.annotation.Nullable; +import android.car.builtin.util.Slog; +import android.content.Context; +import android.opengl.GLES30; +import android.opengl.Matrix; +import android.os.Build; + +import com.android.car.CarLog; + +import libcore.io.Streams; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +/** + * A helper class for simple OpenGL operations + */ +public class GLHelper { + + private static final String TAG = CarLog.tagFor(GLHelper.class); + private static final int SIZEOF_FLOAT = 4; + + /** + * Creates an OpenGL program that uses the provided shader sources and returns the id of the + * created program + * + * @param vertexShaderSource The source for the vertex shader + * @param fragmentShaderSource The source for the fragment shader + * @return The id of the created program + */ + public static int createProgram(String vertexShaderSource, String fragmentShaderSource) { + int vertexShader = compileShader(GLES30.GL_VERTEX_SHADER, vertexShaderSource); + int fragmentShader = compileShader(GLES30.GL_FRAGMENT_SHADER, fragmentShaderSource); + + int programId = GLES30.glCreateProgram(); + checkGlErrors("glCreateProgram"); + + GLES30.glAttachShader(programId, vertexShader); + GLES30.glAttachShader(programId, fragmentShader); + + // glDeleteShader flags these shaders to be deleted, the shaders + // are not actually deleted until the program they are attached to are deleted + GLES30.glDeleteShader(vertexShader); + checkGlErrors("glDeleteShader"); + GLES30.glDeleteShader(fragmentShader); + checkGlErrors("glDeleteShader"); + + GLES30.glLinkProgram(programId); + checkGlErrors("glLinkProgram"); + + return programId; + } + + /** + * Creates and binds a texture and returns the id of the created texture + * + * @param textureIdBuffer The IntBuffer that will contain the created texture id + * @param textureTarget The texture target for the created texture + * @return The id of the created and bound texture + */ + public static int createAndBindTextureObject(IntBuffer textureIdBuffer, int textureTarget) { + GLES30.glGenTextures(1, textureIdBuffer); + + int textureId = textureIdBuffer.get(0); + + GLES30.glBindTexture(textureTarget, textureId); + checkGlErrors("glBindTexture"); + + // We define the filters that will be applied to the textures if + // they get minified or magnified when they are sampled + GLES30.glTexParameterf(textureTarget, GLES30.GL_TEXTURE_MIN_FILTER, + GLES30.GL_LINEAR); + GLES30.glTexParameterf(textureTarget, GLES30.GL_TEXTURE_MAG_FILTER, + GLES30.GL_LINEAR); + + // Set the wrap parameters for if the edges of the texture do not fill the surface + GLES30.glTexParameteri(textureTarget, GLES30.GL_TEXTURE_WRAP_S, + GLES30.GL_CLAMP_TO_EDGE); + GLES30.glTexParameteri(textureTarget, GLES30.GL_TEXTURE_WRAP_T, + GLES30.GL_CLAMP_TO_EDGE); + + return textureId; + } + + /** + * Creates and binds a Framebuffer object + * + * @param frameBuffer the IntBuffer that will contain the created Framebuffer ID + * @return The id of the created and bound Framebuffer + */ + public static int createAndBindFramebuffer(IntBuffer frameBuffer) { + GLES30.glGenFramebuffers(1, frameBuffer); + checkGlErrors("glGenFramebuffers"); + + int frameBufferId = frameBuffer.get(0); + + GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, frameBufferId); + checkGlErrors("glBindFramebuffer"); + + return frameBufferId; + } + + /** + * Retrieves a string of an OpenGL shader + * + * @param id the ID of the raw shader resource + * @return The shader script, null if the shader failed to load + */ + public static @Nullable String getShaderFromRaw(Context context, int id) { + try { + InputStream stream = context.getResources().openRawResource(id); + return new String(Streams.readFully(new InputStreamReader(stream))); + } catch (IOException e) { + Slog.e(TAG, "Failed to load shader"); + return null; + } + } + + /** + * Creates a FloatBuffer to hold texture and vertex coordinates + * + * @param coords The coordinates that will be held in the FloatBuffer + * @return a FloatBuffer containing the provided coordinates + */ + public static FloatBuffer createFloatBuffer(float[] coords) { + ByteBuffer byteBuffer = ByteBuffer.allocateDirect(coords.length * SIZEOF_FLOAT); + byteBuffer.order(ByteOrder.nativeOrder()); + + FloatBuffer floatBuffer = byteBuffer.asFloatBuffer(); + floatBuffer.put(coords); + floatBuffer.position(0); + return floatBuffer; + } + + /** + * @return a float[] representing a 4x4 identity matrix + */ + public static float[] getIdentityMatrix() { + float[] identityMatrix = new float[16]; + Matrix.setIdentityM(identityMatrix, 0); + return identityMatrix; + } + + /** + * Checks for GL errors, logging any errors found + * + * @param func The name of the most recent GL function called + * @return a boolean representing if there was a GL error or not + */ + public static boolean checkGlErrors(String func) { + boolean hadError = false; + int error; + + while ((error = GLES30.glGetError()) != GLES30.GL_NO_ERROR) { + if (Build.IS_ENG || Build.IS_USERDEBUG) { + Slog.e(TAG, func + " failed: error " + error, new Throwable()); + } + hadError = true; + } + return hadError; + } + + private static int compileShader(int shaderType, String shaderSource) { + int shader = GLES30.glCreateShader(shaderType); + GLES30.glShaderSource(shader, shaderSource); + + GLES30.glCompileShader(shader); + checkGlErrors("glCompileShader"); + + return shader; + } +} |