1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car.settings.deeplink; 18 19 import static android.provider.Settings.ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY; 20 import static android.provider.Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY; 21 import static android.provider.Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI; 22 23 import static com.android.car.settings.CarSettingsApplication.CAR_SETTINGS_PACKAGE_NAME; 24 25 import android.annotation.NonNull; 26 import android.content.ComponentName; 27 import android.content.Intent; 28 import android.content.pm.ActivityInfo; 29 import android.net.Uri; 30 import android.util.Log; 31 32 import androidx.window.embedding.SplitRule; 33 34 import com.android.car.settings.activityembedding.ActivityEmbeddingRulesController; 35 import com.android.car.settings.activityembedding.ActivityEmbeddingUtils; 36 import com.android.car.settings.common.CarSettingActivities; 37 import com.android.car.settings.common.Logger; 38 39 import java.net.URISyntaxException; 40 41 /** 42 * {@link CarSettingActivities.HomepageActivity} for when dual-pane is enable via ActivityEmbedding. 43 * This class should only be enabled when dual-pane is active, and should only host activities 44 * that have the permission {@link android.permission.LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK}. 45 * Intents that target an Activity defined within this package, such as subclasses of 46 * {@link com.android.car.settings.common.BaseCarSettingsActivity}, will directly be started in dual 47 * pane unless it is targeting a single pane Activity. There is no need to convert these Intents 48 * using {@code convertToDeepLinkHomepageIntent}. 49 * Other Intents that target external setting defined outside of this package, such as those added 50 * by {@link com.android.car.settings.common.ExtraSettingsLoader}, should be started using a similar 51 * scheme as defined in the method {@code convertToDeepLinkHomepageIntent}. 52 */ 53 public class DeepLinkHomepageActivity extends CarSettingActivities.HomepageActivity { 54 public static final String EMBEDDED_DEEPLINK_INTENT_DATA = 55 "com.android.car.settings.EMBEDDED_DEEPLINK_INTENT_DATA"; 56 public static final String EXTRA_TARGET_SECONDARY_CONTAINER = 57 "com.android.car.settings.EXTRA_TARGET_SECONDARY_CONTAINER"; 58 private static final String TAG = "DeepLinkHomepageActivity"; 59 60 private static final Logger LOG = new Logger(DeepLinkHomepageActivity.class); 61 62 @Override handleNewIntent(Intent intent)63 protected void handleNewIntent(Intent intent) { 64 super.handleNewIntent(intent); 65 maybeLaunchDeepLinkActivity(intent); 66 } 67 68 /** 69 * Launches deep link activity to be directly two pane if it is supported. 70 */ maybeLaunchDeepLinkActivity(Intent intent)71 private void maybeLaunchDeepLinkActivity(Intent intent) { 72 if (!ActivityEmbeddingUtils.isEmbeddingActivityEnabled(this)) { 73 Log.e(TAG, "Embedding is not enabled. Finishing DeepLinkHomepageActivity."); 74 finish(); 75 } 76 Intent targetIntent; 77 try { 78 String intentUriString = intent.getStringExtra( 79 EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI); 80 if (intentUriString == null) { 81 LOG.e("Unable to parse trampoline intent. Intent URI is null"); 82 return; 83 } 84 targetIntent = Intent.parseUri(intentUriString, Intent.URI_INTENT_SCHEME); 85 targetIntent.setData( 86 intent.getParcelableExtra(EMBEDDED_DEEPLINK_INTENT_DATA, Uri.class)); 87 } catch (URISyntaxException e) { 88 LOG.e("Error parsing trampoline intent: " + e); 89 return; 90 } 91 ComponentName targetComponentName = targetIntent.resolveActivity(getPackageManager()); 92 targetIntent.setComponent(targetComponentName); 93 targetIntent.removeFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT); 94 targetIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); 95 targetIntent.replaceExtras(intent); 96 ActivityEmbeddingRulesController.registerDualPaneSplitRule(this, 97 /* primaryComponent= */ new ComponentName(getApplicationContext(), getClass()), 98 /* secondaryComponent= */ targetComponentName, 99 /* secondaryIntentAction= */ targetIntent.getAction(), 100 /* finishPrimaryWithSecondary= */ SplitRule.FinishBehavior.ALWAYS, 101 /* finishSecondaryWithPrimary= */ SplitRule.FinishBehavior.ALWAYS, 102 /* clearTop= */ true); 103 setTopLevelHeaderKey(getTopLevelHeaderKey(targetIntent)); 104 targetIntent.putExtra(EXTRA_TARGET_SECONDARY_CONTAINER, true); 105 startActivity(targetIntent); 106 } 107 108 /** 109 * Retrieves the top level header key from the deeplink Intent. If it is available via resolved 110 * {@link ActivityInfo}, then we prioritize displaying that over the intent extra. 111 */ getTopLevelHeaderKey(Intent targetIntent)112 private String getTopLevelHeaderKey(Intent targetIntent) { 113 String metaDataHeaderKey = null; 114 ActivityInfo ai = getActivityInfo(getPackageManager(), targetIntent.getComponent()); 115 if (ai != null && ai.metaData != null) { 116 metaDataHeaderKey = ai.metaData.getString(META_DATA_KEY_HEADER_KEY); 117 } 118 String deepLinkExtraHeaderKey = targetIntent.getStringExtra( 119 EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY); 120 return metaDataHeaderKey != null ? metaDataHeaderKey : deepLinkExtraHeaderKey; 121 } 122 123 /** 124 * Converts an {@link Intent} to a deeplink intent that, upon being started, will be 125 * trampolined and then handled by this class {@link DeepLinkHomepageActivity}. 126 */ 127 @NonNull convertToDeepLinkHomepageIntent(@onNull Intent targetIntent)128 public static Intent convertToDeepLinkHomepageIntent(@NonNull Intent targetIntent) { 129 targetIntent = new Intent(targetIntent); 130 targetIntent.setSelector(null); 131 132 Intent trampolineIntent = new Intent(ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY); 133 trampolineIntent.setPackage(CAR_SETTINGS_PACKAGE_NAME); 134 trampolineIntent.replaceExtras(targetIntent); 135 trampolineIntent.putExtra(EMBEDDED_DEEPLINK_INTENT_DATA, targetIntent.getData()); 136 // If Intent#getData() is not null, Intent#toUri will return an Uri which has the scheme 137 // of Intent#getData(), and it may not be the scheme of the original Intent (i.e http: 138 // instead of ACTION_VIEW. 139 targetIntent.setData(null); 140 trampolineIntent.putExtra(EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI, 141 targetIntent.toUri(Intent.URI_INTENT_SCHEME)); 142 143 return trampolineIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT 144 | Intent.FLAG_ACTIVITY_NEW_TASK); 145 } 146 } 147