blob: 0d1a7b83c85cfab108ed9f1bb2ef6b5df3bbb119 [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.carlauncher;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.ActivityView;
import android.app.ActivityTaskManager;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.widget.FrameLayout;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentTransaction;
import com.android.car.media.common.PlaybackFragment;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Basic Launcher for Android Automotive which demonstrates the use of {@link ActivityView} to host
* maps content.
*
* <p>Note: On some devices, the ActivityView may render with a width, height, and/or aspect
* ratio that does not meet Android compatibility definitions. Developers should work with content
* owners to ensure content renders correctly when extending or emulating this class.
*
* <p>Note: Since the hosted maps Activity in ActivityView is currently in a virtual display, the
* system considers the Activity to always be in front. Launching the maps Activity with a direct
* Intent will not work. To start the maps Activity on the real display, send the Intent to the
* Launcher with the {@link Intent#CATEGORY_APP_MAPS} category, and the launcher will start the
* Activity on the real display.
*
* <p>Note: The state of the virtual display in the ActivityView is nondeterministic when
* switching away from and back to the current user. To avoid a crash, this Activity will finish
* when switching users.
*/
public class CarLauncher extends FragmentActivity {
private static final String TAG = "CarLauncher";
private static final boolean DEBUG = false;
private ActivityView mActivityView;
private boolean mActivityViewReady;
private boolean mIsStarted;
/** Set to {@code true} once we've logged that the Activity is fully drawn. */
private boolean mIsReadyLogged;
private final ActivityView.StateCallback mActivityViewCallback =
new ActivityView.StateCallback() {
// EmptyActivity last task ID. Used to prevent this task to be moved to front.
// This value can't be cached since it will be concurrently accessed.
private final AtomicInteger mEmptyActivityTaskId = new AtomicInteger(
ActivityTaskManager.INVALID_TASK_ID);
@Override
public void onActivityViewReady(ActivityView view) {
if (DEBUG) Log.d(TAG, "onActivityViewReady(" + getUserId() + ")");
mActivityViewReady = true;
startMapsInActivityView();
maybeLogReady();
}
@Override
public void onActivityViewDestroyed(ActivityView view) {
if (DEBUG) Log.d(TAG, "onActivityViewDestroyed(" + getUserId() + ")");
mActivityViewReady = false;
}
@Override
public void onTaskMovedToFront(int taskId) {
if (DEBUG) {
Log.d(TAG, "onTaskMovedToFront(" + getUserId() + ") with taskId= " +
+ taskId + "invoked on " + "CarLauncher(mIsStarted=" + mIsStarted
+ ", mActivityViewReady=" + mActivityViewReady
+ ", mIsStarted=" + mIsStarted
+ "mEmptyActivityTaskId=" + mEmptyActivityTaskId + ")");
}
// Skip EmptyActivity, since we don't want to move CarLauncher forward for it.
if (mEmptyActivityTaskId.get() == taskId) {
return;
}
try {
if (mActivityViewReady && !mIsStarted) {
ActivityManager am =
(ActivityManager) getSystemService(ACTIVITY_SERVICE);
am.moveTaskToFront(CarLauncher.this.getTaskId(), /* flags= */ 0);
}
} catch (RuntimeException e) {
Log.w(TAG, "Failed to move CarLauncher to front.");
}
}
@Override
public void onTaskCreated(int taskId, ComponentName componentName) {
if (DEBUG) {
Log.d(TAG, "onTaskCreated(" + taskId + ", " + componentName + ")");
}
if (componentName == null) {
return;
}
if (EmptyActivity.class.getName().equals(componentName.getClassName())) {
mEmptyActivityTaskId.set(taskId);
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Don't show the maps panel in multi window mode.
// NOTE: CTS tests for split screen are not compatible with activity views on the default
// activity of the launcher
if (isInMultiWindowMode() || isInPictureInPictureMode()) {
setContentView(R.layout.car_launcher_multiwindow);
} else {
setContentView(R.layout.car_launcher);
}
initializeFragments();
mActivityView = findViewById(R.id.maps);
if (mActivityView != null) {
mActivityView.setCallback(mActivityViewCallback);
}
}
@Override
protected void onNewIntent(Intent intent) {
Set<String> categories = intent.getCategories();
if (categories != null && categories.size() == 1 && categories.contains(
Intent.CATEGORY_APP_MAPS)) {
launchMapsActivity();
}
}
@Override
protected void onRestart() {
super.onRestart();
startMapsInActivityView();
}
@Override
protected void onStart() {
super.onStart();
mIsStarted = true;
maybeLogReady();
// Request EmptyActivity to finish
sendIntentToEmptyActivity(/* stopActivity= */ true);
}
@Override
protected void onStop() {
super.onStop();
mIsStarted = false;
// Request EmptyActivity to start
sendIntentToEmptyActivity(/* stopActivity= */ false);
}
private void sendIntentToEmptyActivity(boolean stopActivity) {
if (DEBUG) Log.d(TAG, "Sending intent to EmptyActivity with stopActivity:" + stopActivity);
if (mActivityView != null && mActivityViewReady) {
if (DEBUG) Log.d(TAG, "Maps exists in CarLauncher");
Intent intent = new Intent(mActivityView.getContext(), EmptyActivity.class);
intent.putExtra(EmptyActivity.EXTRA_STOP_ACTIVITY, stopActivity);
try {
mActivityView.startActivity(intent);
} catch (ActivityNotFoundException e) {
Log.e(TAG, "EmptyActivity not found", e);
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mActivityView != null && mActivityViewReady) {
mActivityView.release();
}
}
/**
* Empty activity used to ensure that onTaskMovedToFront is invoked when the Activity inside
* the ActivityView gets the intent while CarLauncher is in the background. See b/154739682
* for more info.
*/
public static final class EmptyActivity extends Activity {
public static final String EXTRA_STOP_ACTIVITY = "EXTRA_STOP_EMPTY_ACTIVITY";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Setting background color to transparent in order to avoid unnecessary flash when
// starting this activity.
getWindow().getDecorView().setBackgroundColor(Color.TRANSPARENT);
handleEmptyActivityIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handleEmptyActivityIntent(intent);
}
private void handleEmptyActivityIntent(Intent intent) {
if (DEBUG) Log.d(TAG, "Received intent: " + intent);
// Finish this activity if required.
boolean stopActivity = intent.getBooleanExtra(EXTRA_STOP_ACTIVITY,
/* defaultValue= */ true);
if (DEBUG) Log.d(TAG, "Requested to stop EmptyActivity: " + stopActivity);
if (stopActivity) {
finish();
}
}
}
private void startMapsInActivityView() {
// If we happen to be be resurfaced into a multi display mode we skip launching content
// in the activity view as we will get recreated anyway.
if (!mActivityViewReady || isInMultiWindowMode() || isInPictureInPictureMode()) {
return;
}
if (mActivityView != null) {
mActivityView.startActivity(getMapsIntent());
}
}
private void launchMapsActivity() {
// Make sure the Activity launches on the current display instead of in the ActivityView
// virtual display.
final ActivityOptions options = ActivityOptions.makeBasic();
options.setLaunchDisplayId(getDisplay().getDisplayId());
startActivity(getMapsIntent(), options.toBundle());
}
private Intent getMapsIntent() {
return Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_MAPS);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
initializeFragments();
}
private void initializeFragments() {
PlaybackFragment playbackFragment = new PlaybackFragment();
ContextualFragment contextualFragment = null;
FrameLayout contextual = findViewById(R.id.contextual);
if(contextual != null) {
contextualFragment = new ContextualFragment();
}
FragmentTransaction fragmentTransaction =
getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.playback, playbackFragment);
if(contextual != null) {
fragmentTransaction.replace(R.id.contextual, contextualFragment);
}
fragmentTransaction.commitNow();
}
/** Logs that the Activity is ready. Used for startup time diagnostics. */
private void maybeLogReady() {
if (DEBUG) {
Log.d(TAG, "maybeLogReady(" + getUserId() + "): activityReady=" + mActivityViewReady
+ ", started=" + mIsStarted + ", alreadyLogged: " + mIsReadyLogged);
}
if (mActivityViewReady && mIsStarted) {
// We should report every time - the Android framework will take care of logging just
// when it's effectively drawn for the first time, but....
reportFullyDrawn();
if (!mIsReadyLogged) {
// ... we want to manually check that the Log.i below (which is useful to show
// the user id) is only logged once (otherwise it would be logged every time the
// user taps Home)
Log.i(TAG, "Launcher for user " + getUserId() + " is ready");
mIsReadyLogged = true;
}
}
}
}