Intent Playground sample application

The app allows a user to explore the behaviour of different
launch modes, task affinities and intent flags.

It displays the current state of all tasks in the application
and their corresponding flags.

It allows the user to launch a set amount of activities on launch.
This bring the user directly into a state where many options for
exploration are available, rather than having to go through a
complicated setup first.

Access the activity field of RecentTaskInfo using reflection and mirror
the ActivityInstanceInfo into our own value object. This breaks
the compile time dependency on the ActivityInstanceInfo api and turns
it into a runtime dependency.

If the api is missing on the device we can still show the task structure
and log an error with the missing activity instance info.

Known bug: The enable suggestion button crashes the application.
Test: Build and Run

Change-Id: Id0274bae159c16aee6dccd805deb53851ffcf21d
This commit is contained in:
Liam Clark
2018-11-09 17:40:35 -08:00
parent 4aec9895c9
commit bd43fbe931
69 changed files with 3730 additions and 245 deletions

View File

@@ -8,6 +8,11 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
LOCAL_STATIC_ANDROID_LIBRARIES := \
android-support-v13 \
android-support-v7-appcompat \
android-support-v4 \
LOCAL_USE_AAPT2 := true
LOCAL_PACKAGE_NAME := IntentPlayground
@@ -17,5 +22,5 @@ LOCAL_SDK_VERSION := current
include $(BUILD_PACKAGE)
# Use the folloing include to make our test apk.
# Use the following include to make our test apk.
include $(call all-makefiles-under,$(LOCAL_PATH))

View File

@@ -16,7 +16,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.intentplayground">
<!-- Used to reorder tasks using android.app.ActivityManager.moveTaskToFront() -->
<uses-permission android:name="android.permission.REORDER_TASKS" />
<!-- Used to display a persistent explanatory window while launching a series of activities -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
@@ -32,6 +35,7 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".RegularActivity" />
<activity
android:name=".SingleTopActivity"
android:launchMode="singleTop" />

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false" android:alpha="?android:attr/disabledAlpha"
android:color="?android:attr/colorControlNormal" />
<item android:state_checked="true" android:color="?android:attr/colorControlActivated" />
<item android:color="?android:attr/colorControlNormal"/>
</selector>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:color="?android:attr/colorControlNormal" />
<item android:color="@color/colorSuggestedCheckBox" />
</selector>

View File

@@ -14,7 +14,14 @@
limitations under the License.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/background_light"/>
<corners android:radius="2dp"/>
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
<solid android:color="@android:color/transparent"/>
<corners android:radius="@dimen/corner_radius"/>
<padding
android:left="@dimen/smallMargin"
android:top="@dimen/smallMargin"
android:right="@dimen/smallMargin"
android:bottom="@dimen/smallMargin" />
</shape>

View File

@@ -14,6 +14,8 @@
limitations under the License.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size android:height="1dp" />
<solid android:color="#ffE0E0E0" />
</shape>

View File

@@ -14,10 +14,11 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF9E9E9E"
android:pathData="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z"/>

View File

@@ -14,10 +14,10 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF9E9E9E"
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M11,18h2v-2h-2v2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM12,6c-2.21,0 -4,1.79 -4,4h2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2c0,2 -3,1.75 -3,5h2c0,-2.25 3,-2.5 3,-5 0,-2.21 -1.79,-4 -4,-4z"/>
</vector>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19,19H5V5h7V3H5c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2v7zM14,3v2h3.59l-9.83,9.83 1.41,1.41L19,6.41V10h2V3h-7z"/>
</vector>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/white"
android:pathData="M19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM14,17L7,17v-2h7v2zM17,13L7,13v-2h10v2zM17,9L7,9L7,7h10v2z"/>
</vector>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12L21,5l-9,-4zM10,17l-4,-4 1.41,-1.41L10,14.17l6.59,-6.59L18,9l-8,8z"/>
</vector>

View File

@@ -14,10 +14,10 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/background_light" />
<corners android:radius="2dp" />
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/shady_grey" />
</shape>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<solid android:color="@color/white" />
<corners android:radius="@dimen/corner_radius"/>
<stroke android:color="@color/colorAccent" android:width="@dimen/border_width" />
<padding
android:left="@dimen/medMargin"
android:top="@dimen/medMargin"
android:right="@dimen/medMargin"
android:bottom="@dimen/medMargin" />
</shape>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/md_grey_900"/>
<corners android:radius="0dp"/>
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/white"/>
<corners android:radius="2dp"/>
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>

View File

@@ -13,19 +13,40 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<ScrollView
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/scroll_container"
tools:context="com.example.android.intentplayground.BaseActivity">
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="visible">
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/toolbarHeight"
android:background="@color/colorPrimary"
android:elevation="@dimen/toolbar_elevation"
app:titleTextColor="@color/white"/>
<ScrollView
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/scroll_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
tools:context="com.example.android.intentplayground.BaseActivity">
<LinearLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:showDividers="middle"
android:divider="@drawable/divider"
android:visibility="visible">
</LinearLayout>
</ScrollView>
</LinearLayout>
</ScrollView>
</FrameLayout>

View File

@@ -34,7 +34,7 @@
android:id="@+id/color_label"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/card_background"
android:src="@drawable/label_background"
android:tint="@color/defaultTint" />
<TextView

View File

@@ -14,7 +14,6 @@
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
@@ -24,6 +23,7 @@
android:id="@+id/radio_launchMode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/fullMargin"
android:tag="activityTag"
android:text="@string/activity_name_placeholder"
android:textAppearance="@style/medium" />

View File

@@ -13,11 +13,10 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<CheckBox
xmlns:android="http://schemas.android.com/apk/res/android"
<CheckBox xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/checkBox_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/checkbox_placeholder"
android:layout_height="@dimen/denseListItem"
android:paddingStart="@dimen/fullMargin"
android:text="@string/placeholder"
android:textAppearance="@style/normal" />

View File

@@ -22,6 +22,4 @@
android:paddingRight="@dimen/fullMargin"
android:text="@string/dialog_intent_flag_placeholder"
android:textAppearance="@style/normal"
android:textSize="@dimen/regularText">
</TextView>
android:textSize="@dimen/regularText" />

View File

@@ -23,7 +23,6 @@
android:layout_marginRight="@dimen/smallMargin"
android:layout_marginStart="@dimen/smallMargin"
android:layout_marginTop="@dimen/smallMargin"
android:background="@drawable/card_background"
android:elevation="@dimen/cardElevation"
android:orientation="vertical"
android:padding="@dimen/medMargin">

View File

@@ -14,7 +14,6 @@
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical" android:layout_width="wrap_content"
android:layout_height="match_parent"
android:minWidth="@dimen/dialogWidth">

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="@+id/result_fragment_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/toolbarHeight"
android:background="@color/colorPrimary"
android:elevation="4dp"
app:title="@string/result_fragment_title"
app:navigationIcon="@drawable/icon_close"
android:foregroundTint="@color/white"
app:titleTextColor="@color/white"/>
<LinearLayout
android:id="@+id/result_fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical" />
</LinearLayout>
</ScrollView>

View File

@@ -32,6 +32,7 @@
android:layout_marginTop="@dimen/medMargin"
android:text="@string/task_tree_title"
android:textAppearance="@style/title" />
<LinearLayout
android:id="@+id/task_tree"
android:layout_width="match_parent"

View File

@@ -13,10 +13,13 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/smallMargin">
android:paddingTop="@dimen/denseSectionMargin"
android:paddingBottom="@dimen/smallMargin">
<TextView
android:id="@+id/header_title"

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="?android:attr/actionBarSize"
android:background="@color/colorPrimaryDark"
android:gravity="bottom"
android:orientation="horizontal">
<Button
android:id="@+id/cancel_pager"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
style="@style/flatButton"
android:text="@string/cancel_button" />
<Button
android:id="@+id/next_pager"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
style="@style/flatButton"
android:text="@string/next_button" />
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="bottom">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/fullMargin"
android:layout_marginBottom="@dimen/tutorial_bottom_margin"
android:elevation="@dimen/tutorial_elevation"
android:background="@drawable/tutorial_card"
android:layout_gravity="bottom|center_horizontal" >
<TextView
android:id="@+id/tutorial_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/md_grey_900"
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
android:padding="@dimen/fullMargin" />
</LinearLayout>
</FrameLayout>
</LinearLayout>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout
android:id="@+id/snack_loading"
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="@dimen/snackbarHeight"
android:paddingHorizontal="@dimen/fullMargin"
android:paddingVertical="@dimen/snackbarVerticalPadding"
android:background="@drawable/snack_background">
<TextView
android:id="@+id/snackbar_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/snackbarText"
android:text=""/>
</LinearLayout>

View File

@@ -14,8 +14,6 @@
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:gravity="center_vertical|left"
android:layout_height="@dimen/listItem" >
@@ -26,7 +24,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/fullMargin"
android:layout_weight="0"
android:text="@string/placeholder"
android:text="@string/task_num"
android:textAppearance="@style/medium"
android:textSize="@dimen/regularText" />

View File

@@ -15,17 +15,11 @@
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/build_intent_container"
android:id="@+id/build_intent_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/smallMargin"
android:layout_marginLeft="@dimen/smallMargin"
android:layout_marginRight="@dimen/smallMargin"
android:layout_marginStart="@dimen/smallMargin"
android:layout_marginTop="@dimen/smallMargin"
android:layout_margin="@dimen/smallMargin"
android:background="@drawable/card_background"
android:elevation="@dimen/cardElevation"
android:orientation="vertical"
android:padding="@dimen/medMargin">
@@ -42,13 +36,45 @@
android:layout_height="wrap_content"
android:text="@string/build_intent_caption" />
<LinearLayout
android:id="@+id/suggest_list_item"
android:layout_width="match_parent"
android:layout_height="@dimen/doubleListItem"
android:orientation="horizontal"
android:paddingTop="@dimen/smallMargin"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_weight="1" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/suggest_enable_text"
android:textAppearance="@style/normal" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/suggest_description_text"
android:textAppearance="@style/caption" />
</LinearLayout>
<Switch
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/suggestion_switch"/>
</LinearLayout>
<LinearLayout
android:id="@+id/build_intent_flags"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="@dimen/smallMargin"
android:paddingTop="@dimen/smallMargin" />
android:paddingBottom="@dimen/smallMargin"/>
<View
android:layout_width="match_parent"

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/app_bar_test"
android:icon="@drawable/icon_tests"
android:iconTint="@color/white"
android:title="@string/run_intent_tests"
android:visible="true"
/>
<item
android:id="@+id/app_bar_help"
android:icon="@drawable/help_outline"
android:title="@string/help_action"
android:iconTint="@color/white"
app:showAsAction="ifRoom" />
</menu>

View File

@@ -17,6 +17,7 @@
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="white">@android:color/white</color>
<!-- Material design colours from
https://material.io/guidelines/style/color.html#color-color-palette -->
<color name="md_red_500">#F44336</color>
@@ -40,4 +41,10 @@
<color name="md_blue_grey_500">#607D8B</color>
<color name="defaultTint">@color/md_teal_500</color>
<color name="labelText">@android:color/white</color>
<color name="md_grey_900">#FF212121</color>
<color name="transparent_grey">#80212121</color>
<color name="colorSuggestedCheckBox">@color/colorAccent</color>
<color name="background">#FAFAFA</color>
<color name="colorAccentTranslucent">#50FF4081</color>
<color name="shady_grey">#AA212121</color>
</resources>

View File

@@ -1,17 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources>
<dimen name="dialogWidth">@android:dimen/dialog_min_width_major</dimen>
<dimen name="dialogWidth">320dp</dimen>
<dimen name="smallMargin">8dp</dimen>
<dimen name="medMargin">16dp</dimen>
<dimen name="fullMargin">24dp</dimen>
<dimen name="blockMargin">32dp</dimen>
<dimen name="cardElevation">2dp</dimen>
<dimen name="blockMargin">56dp</dimen>
<dimen name="cardElevation">0dp</dimen>
<dimen name="titleText">20sp</dimen>
<dimen name="regularText">16sp</dimen>
<dimen name="smallText">12sp</dimen>
<dimen name="listItem">48dp</dimen>
<dimen name="denseListItem">40dp</dimen>
<dimen name="listIcon">48dp</dimen>
<dimen name="listControl">24dp</dimen>
<dimen name="listLabelTextBox">24dp</dimen>
<dimen name="dividerHeight">1dp</dimen>
</resources>
<dimen name="toolbarHeight">56dp</dimen>
<dimen name="snackbarHeight">48dp</dimen>
<dimen name="snackbarVerticalPadding">14dp</dimen>
<dimen name="bottom_nav_elevation">8dp</dimen>
<dimen name="checkbox_padding">24dp</dimen>
<dimen name="toolbar_elevation">4dp</dimen>
<dimen name="doubleListItem">60dp</dimen>
<dimen name="denseSectionMargin">12dp</dimen>
<dimen name="border_width">1dp</dimen>
<dimen name="tutorial_elevation">4dp</dimen>
<dimen name="corner_radius">2dp</dimen>
<dimen name="tutorial_bottom_margin">100dp</dimen>
</resources>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources>
<item type="id" name="tag_flag" />
<item type="id" name="tag_suggested" />
<item type="id" name="build_intent_container" />
<item type="id" name="fragment_container_bottom" />
</resources>

View File

@@ -14,7 +14,7 @@
limitations under the License.
-->
<resources>
<string name="app_name">Intent Playground</string>
<string name="app_name">Intents &amp; Activities</string>
<string name="task_id">com.example.android.intentplayground.taskId</string>
<string name="activity_index">com.example.android.intentplayground.activityIndex</string>
<string name="activity_name">CompName</string>
@@ -42,24 +42,50 @@
<string name="action_label_description">Label for the action of the current intent</string>
<string name="action_label">Action</string>
<string name="action_placeholder">which action?</string>
<string name="data_uri_label_description">Label for the action of the current intent</string>
<string name="data_uri_label_description">Label for the data URI of the current intent</string>
<string name="data_uri_label">Data URI</string>
<string name="data_uri_placeholder">://</string>
<string name="type_label_description">Label for the action of the current intent</string>
<string name="type_label_description">Label for the type of the current intent</string>
<string name="type_label">Type</string>
<string name="type_placeholder">which action?</string>
<string name="package_label_description">Label for the action of the current intent</string>
<string name="package_label_description">Label for the package of the current intent</string>
<string name="package_label">Package</string>
<string name="package_placeholder">which action?</string>
<string name="category_label_description">Label for the action of the current intent</string>
<string name="category_label_description">Label for the categories of the current intent</string>
<string name="category_label">Categories</string>
<string name="flags_label_description">Label for the action of the current intent</string>
<string name="flags_label_description">Label for the flags of the current intent</string>
<string name="flags_label">Flags</string>
<string name="task_tree_title">Hierarchy</string>
<string name="intent_section_title">Title</string>
<string name="placeholder">placeholder</string>
<string name="kill_task_button">remove</string>
<string name="move_to_front">Move to front</string>
<string name="current_task_hierarchy_title">Current Hierarchy</string>
<string name="expected_task_hierarchy_title">Expected Hierarchy</string>
<string name="current_task_hierarchy_title">Current Tasks</string>
<string name="expected_task_hierarchy_title">Expected Tasks</string>
<string name="launch_wait_toast">Please wait while the initial tasks are launched.</string>
<string name="help_action">Show help dialog</string>
<string name="package_uri_scheme">package</string>
<string name="ask_to_launch">Do you want to launch a preset stack of activities?</string>
<string name="ask_to_launch_affirm">Yes (recommended)</string>
<string name="ask_to_launch_cancel">No</string>
<string name="next_button">Next</string>
<string name="cancel_button">cancel</string>
<string name="help_step_one">Here you can see the tasks currently running in this application, and rearrange or remove them.</string>
<string name="task_num">Task #</string>
<string name="help_step_finish">Back to app</string>
<string name="help_step_two">The current intent pane displays information about the intent that launched this activity.</string>
<string name="help_step_three">Select flags for a new intent.</string>
<string name="help_step_four">Select a target activity and launch it.</string>
<string name="bottom_nav_launch">Launch</string>
<string name="bottom_nav_verify">Verify</string>
<string name="launch_explanation">If you click yes, a few different tasks will be launched, which you can then manage and play with using intent flags and the app UI.</string>
<string name="result_fragment_title">Result</string>
<string name="suggest_enable_text">Enable suggestions</string>
<string name="suggest_description_text">As you select flags, automatically check dependent flags, highlight suggested flags, and disable conflicting ones.</string>
<string name="dialog_intent_flags">intent flags</string>
<string name="no_activities_text">No activities</string>
<string name="one_activity_text">1 activity</string>
<string name="plural_activities_text">%d activities</string>
<string name="new_task">NEW</string>
<string name="run_intent_tests">Run Intent tests</string>
</resources>

View File

@@ -15,10 +15,11 @@
-->
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="@android:style/Theme.Material.Light">
<style name="AppTheme" parent="@style/Theme.AppCompat.Light.NoActionBar">
<item name="android:colorPrimary">@color/colorPrimary</item>
<item name="android:colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="android:colorAccent">@color/colorAccent</item>
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
</style>
<style name="flatButton" parent="@android:style/Widget.Material.Light.Button.Borderless.Colored" />
<style name="title" parent="@android:style/TextAppearance.Material.Title" />
@@ -26,4 +27,12 @@
<style name="medium" parent="@android:style/TextAppearance.Material.Body2" />
<style name="caption" parent="@android:style/TextAppearance.Material.Caption" />
<style name="subsection" parent="@android:style/TextAppearance.Material.Medium" />
<style name="toolbarTheme" parent="@android:style/ThemeOverlay.Material.Light" />
<style name="snackbarText" parent="@android:style/TextAppearance.Material.Body1">
<item name="android:textSize">@dimen/snackBarText</item>
<item name="android:textColor">@color/white</item>
</style>
<dimen name="snackBarText">14dp</dimen>
</resources>

View File

@@ -0,0 +1,84 @@
/*
* 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.example.android.intentplayground;
import android.content.ComponentName;
import android.content.Intent;
import android.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* Provides a backend for launching activities using the ActivityManager command line.
*/
class AMControl {
public static final String TAG = "AMControl";
/**
* Launches the activity specified by an {@link Intent} in the background.
* @param intent The intent to launch.
* @return The output of the "am shell" command.
*/
public static String launchInBackground(Intent intent) {
StringBuilder cmd = new StringBuilder("am start -n ");
ComponentName target = intent.getComponent();
cmd.append(target.getPackageName()).append("/").append(target.getShortClassName());
cmd.append(" -f ").append("0x").append(Integer.toHexString(intent.getFlags()));
cmd.append(" --ez moveToBack true");
return execCmd(cmd.toString());
}
/**
* Executes a shell command in a separate process.
* @param cmd The command to execute.
* @return The output of the command.
*/
public static String execCmd(String cmd) {
StringBuilder output = new StringBuilder();
ProcessBuilder factory = new ProcessBuilder(cmd.split(" "));
String line;
int lineCount = 0;
if (BuildConfig.DEBUG) Log.d(TAG, "Running command " + cmd);
try {
Process proc = factory.start();
// get stdout
BufferedReader reader = new BufferedReader(new InputStreamReader(
proc.getInputStream()));
while ((line = reader.readLine()) != null) {
output.append(line).append('\n');
lineCount++;
}
reader.close();
// get stderr
reader = new BufferedReader(new InputStreamReader(proc.getErrorStream()));
while ((line = reader.readLine()) != null) {
output.append(line).append('\n');
lineCount++;
}
reader.close();
if (BuildConfig.DEBUG) {
Log.d(TAG, String.format("Received %d lines from %s:\n %s",
lineCount, cmd.split(" ")[0], output.toString()));
}
} catch (IOException e) {
if (BuildConfig.DEBUG) Log.e(TAG, output.append(e.getMessage()).toString());
throw new RuntimeException(e);
}
return output.toString();
}
}

View File

@@ -0,0 +1,27 @@
/*
* 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.example.android.intentplayground;
/**
* Created by wvk on 7/25/17.
*/
enum ActivityFlag {
CLEAR_TASK_ON_LAUNCH, ALLOW_TASK_REPARENTING, LAUNCH_MODE_STANDARD, LAUNCH_MODE_SINGLE_TOP,
LAUNCH_MODE_SINGLE_TASK, LAUNCH_MODE_SINGLE_INSTANCE, DOCUMENT_LAUNCH_MODE_INTO_EXISTING,
DOCUMENT_LAUNCH_MODE_ALWAYS, DOCUMENT_LAUNCH_MODE_NONE, DOCUMENT_LAUNCH_MODE_NEVER;
}

View File

@@ -16,106 +16,97 @@
package com.example.android.intentplayground;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.ComponentName;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* All of the other activities extend BaseActivity, the shared functionality is implemented here
* Implements the shared functionality for all of the other activities.
*/
public abstract class BaseActivity extends Activity {
public final static String LAUNCH_FORWARD = "com.example.android.launchForward";
public final static String BUILDER_FRAGMENT = "com.example.android.builderFragment";
protected ComponentName mActivityToLaunch;
protected List<ActivityManager.AppTask> mTasks;
public abstract class BaseActivity extends AppCompatActivity implements
IntentBuilderView.OnLaunchCallback {
public final static String EXTRA_LAUNCH_FORWARD = "com.example.android.launchForward";
public final static String BUILDER_VIEW = "com.example.android.builderFragment";
public static final String TREE_FRAGMENT = "com.example.android.treeFragment";
public static final String EXPECTED_TREE_FRAGMENT = "com.example.android.expectedTreeFragment";
public static final int LAUNCH_REQUEST_CODE = 0xEF;
public enum Mode {LAUNCH, VERIFY, RESULT}
public boolean userLeaveHintWasCalled = false;
protected Mode mStatus = Mode.LAUNCH;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (BuildConfig.DEBUG) Log.d(this.getLocalClassName(), "onCreate()");
if (BuildConfig.DEBUG) Log.d(getLocalClassName(), "onCreate()");
// Setup action bar
Toolbar appBar = (Toolbar) findViewById(R.id.app_bar);
setSupportActionBar(appBar);
loadMode(Mode.LAUNCH);
}
@Override
protected void onResume() {
super.onResume();
Intent launchForward = prepareLaunchForward();
if (launchForward != null) {
startActivity(launchForward);
}
}
/**
* Initializes the UI for the specified {@link Mode}.
* @param mode The mode to display.
*/
protected void loadMode(Mode mode) {
Intent intent = getIntent();
ViewGroup container = (ViewGroup) findViewById(R.id.fragment_container);
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(R.id.fragment_container, new CurrentTaskFragment());
TreeFragment currentTaskFrag = new TreeFragment();
Bundle args = new Bundle();
args.putString(TreeFragment.FRAGMENT_TITLE,
getString(R.string.current_task_hierarchy_title));
currentTaskFrag.setArguments(args);
transaction.add(R.id.fragment_container, currentTaskFrag);
if (intent.hasExtra(TestBase.EXPECTED_HIERARCHY)) {
// That means this activity was launched as a test show the result fragment
TreeFragment expectedView = new TreeFragment();
Bundle expectedArgs = new Bundle();
expectedArgs.putParcelable(TreeFragment.TREE_NODE,
intent.getParcelableExtra(TestBase.EXPECTED_HIERARCHY));
expectedArgs.putString(TreeFragment.FRAGMENT_TITLE,
getString(R.string.expected_task_hierarchy_title));
expectedView.setArguments(expectedArgs);
transaction.add(R.id.fragment_container, expectedView);
}
transaction.add(R.id.fragment_container, new IntentFragment());
transaction.add(R.id.fragment_container, new IntentBuilderFragment(), BUILDER_FRAGMENT);
transaction.commit();
if (intent.hasExtra(LAUNCH_FORWARD)) {
ArrayList<Intent> intents = intent.getParcelableArrayListExtra(LAUNCH_FORWARD);
if (!intents.isEmpty()) {
Intent nextIntent = intents.remove(0);
nextIntent.putParcelableArrayListExtra(LAUNCH_FORWARD, intents);
if (BuildConfig.DEBUG) {
Log.d(this.getLocalClassName(),
LAUNCH_FORWARD + " " + nextIntent.getComponent().toString());
}
startActivity(nextIntent);
FragmentTransaction transaction = fragmentManager.beginTransaction()
.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out);
if (mode == Mode.LAUNCH) {
transaction.replace(R.id.fragment_container, new CurrentTaskFragment());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
TreeFragment currentTaskFragment = new TreeFragment();
Bundle args = new Bundle();
args.putString(TreeFragment.FRAGMENT_TITLE,
getString(R.string.current_task_hierarchy_title));
currentTaskFragment.setArguments(args);
transaction.add(R.id.fragment_container, currentTaskFragment, TREE_FRAGMENT);
}
transaction.add(R.id.fragment_container, new IntentFragment());
transaction.commit();
// Ensure IntentBuilderView is last by adding it to the container after commit()
transaction.runOnCommit(() -> {
IntentBuilderView builderView = new IntentBuilderView(this, mode);
builderView.setOnLaunchCallback(this::launchActivity);
View bottomAnchorView = new View(this);
bottomAnchorView.setId(R.id.fragment_container_bottom);
container.addView(builderView);
container.addView(bottomAnchorView);
});
mStatus = Mode.LAUNCH;
}
}
/**
* Launches activity with the selected options
* Launches activity with the selected options.
*/
public void launchActivity(View view) {
Intent customIntent = new Intent();
LinearLayout flagBuilder = findViewById(R.id.build_intent_flags);
// Gather flags from flag builder checkbox list
childrenOfGroup(flagBuilder, CheckBox.class)
.forEach(checkbox -> {
int flagVal = FlagUtils.value(checkbox.getText().toString());
if (checkbox.isChecked()) customIntent.addFlags(flagVal);
else customIntent.removeFlags(flagVal);
});
customIntent.setComponent(mActivityToLaunch);
startActivity(customIntent);
}
/**
* Convenience method to retrieve children of a certain type from a {@link ViewGroup}
* @param group the ViewGroup to retrieve children from
*/
protected static <T> List<T> childrenOfGroup(ViewGroup group, Class<T> viewType) {
List<T> list = new LinkedList<>();
for (int i = 0; i < group.getChildCount(); i++) {
View v = group.getChildAt(i);
if (viewType.isAssignableFrom(v.getClass())) list.add(viewType.cast(v));
}
return list;
public void launchActivity(Intent intent) {
startActivity(intent);
}
@Override
@@ -123,4 +114,88 @@ public abstract class BaseActivity extends Activity {
super.onNewIntent(intent);
setIntent(intent);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.app_bar, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.app_bar_help:
showHelpDialog();
break;
case R.id.app_bar_test:
runIntentTests();
break;
}
return super.onOptionsItemSelected(item);
}
protected void runIntentTests() {
startActivity(getPackageManager()
.getLaunchIntentForPackage("com.example.android.intentplayground.test"));
}
/**
* Creates and displays a help overlay on this activity.
*/
protected void showHelpDialog() {
FragmentManager fragmentManager = getFragmentManager();
LinearLayout container = (LinearLayout) findViewById(R.id.fragment_container);
container.setShowDividers(LinearLayout.SHOW_DIVIDER_NONE);
ShowcaseFragment demo = new ShowcaseFragment();
demo.addStep(R.string.help_step_one, R.id.task_tree_container, () -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
TreeFragment frag = (TreeFragment) fragmentManager.findFragmentByTag(TREE_FRAGMENT);
if (frag != null) {
frag.openTask(0);
frag.openTask(1);
}
}
});
demo.addStep(R.string.help_step_two, R.id.intent_container);
demo.addStep(R.string.help_step_three, R.id.build_intent_container,
R.id.build_intent_view);
demo.addStep(R.string.help_step_four, R.id.fragment_container_bottom,
R.id.launch_button);
demo.setScroller((ScrollView) findViewById(R.id.scroll_container));
demo.setOnFinish(() -> container.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE));
fragmentManager.beginTransaction()
.add(R.id.root_container, demo)
.addToBackStack(null)
.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out)
.commit();
}
protected Intent prepareLaunchForward() {
Intent intent = getIntent();
Intent nextIntent = null;
if (intent.hasExtra(EXTRA_LAUNCH_FORWARD)) {
Log.e(getLocalClassName(), "It's happening! LAUNCH_FORWARD");
ArrayList<Intent> intents = intent.getParcelableArrayListExtra(EXTRA_LAUNCH_FORWARD);
if (!intents.isEmpty()) {
nextIntent = intents.remove(0);
nextIntent.putParcelableArrayListExtra(EXTRA_LAUNCH_FORWARD, intents);
if (BuildConfig.DEBUG) {
Log.d(getLocalClassName(), EXTRA_LAUNCH_FORWARD + " "
+ nextIntent.getComponent().toString());
}
}
}
return nextIntent;
}
/**
* Sets a public field for the purpose of testing.
*/
@Override
protected void onUserLeaveHint() {
super.onUserLeaveHint();
userLeaveHintWasCalled = true;
}
}

View File

@@ -17,6 +17,6 @@
package com.example.android.intentplayground;
/**
* An activity with clearTaskOnLaunch flag set in AndroidManifest.xml
* An activity with clearTaskOnLaunch flag set in AndroidManifest.xml.
*/
public class ClearTaskOnLaunchActivity extends BaseActivity {}

View File

@@ -0,0 +1,91 @@
/*
* 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.example.android.intentplayground;
import android.content.ComponentName;
import android.support.annotation.ColorRes;
import java.util.HashMap;
import java.util.Map;
/**
* Assigns colors to given tasks and activities.
*/
public class ColorManager {
private static Map<String, Integer> mActivityColorMap = new HashMap<>();
private static Map<Integer, Integer> mTaskColorMap = new HashMap<>();
private static int[] mColors = new int[] {
R.color.md_red_500,
R.color.md_pink_500,
R.color.md_purple_500,
R.color.md_deep_purple_500,
R.color.md_indigo_500,
R.color.md_blue_500,
R.color.md_light_blue_500,
R.color.md_cyan_500,
R.color.md_green_500,
R.color.md_light_green_500,
R.color.md_lime_500,
R.color.md_yellow_500,
R.color.md_amber_500,
R.color.md_orange_500,
R.color.md_deep_orange_500,
R.color.md_brown_500,
R.color.md_blue_grey_500
};
private static int mLastTaskColor = -1;
private static int mLastActivityColor = -1;
/**
* Retrieves the assigned color for the given activity.
* @param activity The activity to retrieve a color for.
* @return The corresponding color for this activity.
*/
public static @ColorRes int getColorForActivity(ComponentName activity) {
String className = activity.getClassName();
if (mActivityColorMap.containsKey(className)) {
return mActivityColorMap.get(className);
} else {
int newColor = nextActivityColor();
mActivityColorMap.put(className, newColor);
return newColor;
}
}
/**
* Retrieves the assigned color for the given task.
* @param taskPersistentId The ID of the task to retrieve a color for.
* @return The corresponding color for this task.
*/
public static @ColorRes int getColorForTask(int taskPersistentId) {
if (mTaskColorMap.containsKey(taskPersistentId)) {
return mTaskColorMap.get(taskPersistentId);
} else {
int newColor = nextTaskColor();
mTaskColorMap.put(taskPersistentId, newColor);
return newColor;
}
}
private static @ColorRes int nextTaskColor() {
return mColors[++mLastTaskColor % mColors.length];
}
private static @ColorRes int nextActivityColor() {
return mColors[++mLastActivityColor % mColors.length];
}
}

View File

@@ -0,0 +1,81 @@
/*
* 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.example.android.intentplayground;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityManager.AppTask;
import android.app.ActivityManager.RecentTaskInfo;
import android.app.Fragment;
import android.content.res.Resources;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.List;
import java.util.Locale;
/**
* Displays details about the current task and activity.
*/
public class CurrentTaskFragment extends Fragment {
private TextView mCurrentTaskView, mCurrentActivityView, mLastTaskView, mLastActivityView;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
Bundle savedInstanceState) {
LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.fragment_current_task,
container, false /* attachToRoot */);
mCurrentTaskView = layout.findViewById(R.id.current_task);
mCurrentActivityView = layout.findViewById(R.id.current_activity);
mLastTaskView = layout.findViewById(R.id.last_task);
mLastActivityView = layout.findViewById(R.id.last_activity);
return layout;
}
@Override
public void onResume() {
super.onResume();
Activity activity = getActivity();
Resources res = activity.getResources();
List<AppTask> tasks = activity.getSystemService(ActivityManager.class).getAppTasks();
RecentTaskInfo currentTask = tasks.get(0).getTaskInfo();
RecentTaskInfo lastTask = tasks.size() > 1 && tasks.get(1) != null ?
tasks.get(1).getTaskInfo() : null;
mCurrentTaskView.setText(String.format(Locale.ENGLISH, "#%d", currentTask.persistentId));
mCurrentTaskView.setTextColor(res.getColor(ColorManager.getColorForTask(
currentTask.persistentId), null /* theme */));
mCurrentActivityView.setText(currentTask.topActivity.getShortClassName());
mCurrentActivityView.setTextColor(res.getColor(ColorManager.getColorForActivity(
currentTask.topActivity), null /* theme */));
if (lastTask != null) {
mLastTaskView.setText(String.format(Locale.ENGLISH, "#%d", lastTask.persistentId));
mLastTaskView.setTextColor(res.getColor(ColorManager.getColorForTask(
lastTask.persistentId), null /* theme */));
if (lastTask.topActivity != null) {
mLastActivityView.setText(lastTask.topActivity.getShortClassName());
mLastActivityView.setTextColor(res.getColor(ColorManager.getColorForActivity(
lastTask.topActivity), null /* theme */));
}
}
}
}

View File

@@ -17,6 +17,6 @@
package com.example.android.intentplayground;
/**
* An activity with documentLaunch="always" set in AndroidManifest.xml
* An activity with documentLaunch="always" set in AndroidManifest.xml.
*/
public class DocumentLaunchAlwaysActivity extends BaseActivity {}

View File

@@ -17,6 +17,6 @@
package com.example.android.intentplayground;
/**
* An activity with documentLaunch="intoExisting" set in AndroidManifest.xml
* An activity with documentLaunch="intoExisting" set in AndroidManifest.xml.
*/
public class DocumentLaunchIntoActivity extends BaseActivity {}

View File

@@ -17,6 +17,6 @@
package com.example.android.intentplayground;
/**
* An activity with documentLaunch="never" set in AndroidManifest.xml
* An activity with documentLaunch="never" set in AndroidManifest.xml.
*/
public class DocumentLaunchNeverActivity extends BaseActivity {}

View File

@@ -0,0 +1,193 @@
/*
* 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.example.android.intentplayground;
import android.app.Activity;
import android.app.FragmentTransaction;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
/**
* A two-level adapter for tasks and the activities that they hold (represented by Node).
*/
class ExpandableAdapter extends BaseExpandableListAdapter {
private Activity mActivity;
private Node mTasks;
/**
* Constructs a new ExpandableAdapter.
* @param activity The activity that holds this adapter.
* @param tasks The {@link Node} root of the task hierarchy.
*/
public ExpandableAdapter(Activity activity, Node tasks) {
mActivity = activity;
mTasks = tasks;
}
@Override
public int getGroupCount() {
return mTasks.mChildren.size();
}
@Override
public int getChildrenCount(int group) {
return mTasks.mChildren.get(group).mChildren.size();
}
@Override
public Object getGroup(int group) {
return mTasks.mChildren.get(group);
}
@Override
public Object getChild(int group, int child) {
return mTasks.mChildren.get(group).mChildren.get(child);
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public View getGroupView(int group, boolean isExpanded, View view, ViewGroup parent) {
String numActivitiesText;
TaskViewHolder holder;
Node task = (Node) getGroup(group);
int nActivities = getChildrenCount(group);
switch (nActivities) {
case 0: numActivitiesText = mActivity.getString(R.string.no_activities_text);
break;
case 1: numActivitiesText = mActivity.getString(R.string.one_activity_text);
break;
default: numActivitiesText = String.format(Locale.ENGLISH,
mActivity.getString(R.string.plural_activities_text), nActivities);
}
if (view == null) {
LayoutInflater inflater = LayoutInflater.from(this.mActivity);
view = inflater.inflate(R.layout.task_node,parent, false);
holder = new TaskViewHolder(view);
view.setTag(holder);
} else {
holder = (TaskViewHolder) view.getTag();
}
if (isExpanded) {
holder.indicatorImageView.setImageResource(R.drawable.expand_less_mtrl);
} else {
holder.indicatorImageView.setImageResource(R.drawable.expand_more_mtrl);
}
holder.taskIdTextView.setText(task.mTaskId == Node.NEW_TASK_ID ?
mActivity.getString(R.string.new_task) : String.valueOf(task.mTaskId));
holder.taskIdTextView.setTextColor(mActivity
.getResources().getColor(ColorManager.getColorForTask(task.mTaskId),
null /* theme */));
holder.numActivitiesTextView.setText(numActivitiesText);
return view;
}
@Override
public View getChildView(int group, int child, boolean lastChild, View view, ViewGroup parent) {
Node activity = (Node) getChild(group, child);
ActivityViewHolder holder;
if (view == null) {
LayoutInflater inflater = LayoutInflater.from(mActivity);
view = inflater.inflate(R.layout.activity_node, parent, false /* attachToRoot */);
holder = new ActivityViewHolder(view);
view.setTag(holder);
} else {
holder = (ActivityViewHolder) view.getTag();
}
holder.activityNameTextView.setText(activity.mName.getShortClassName());
holder.activityNumTextView.setText(String.format(Locale.ENGLISH, "%d",
getChildrenCount(group) - child));
int color = mActivity.getResources()
.getColor(ColorManager.getColorForActivity(activity.mName), null /* theme */);
holder.labelImageView.setColorFilter(color);
holder.intentButtonView.setColorFilter(color);
holder.intentButtonView.setOnClickListener(clickedView -> {
Intent intent = ((Node) getChild(group, child)).getIntent();
List<String> flags;
if (intent != null) {
flags = FlagUtils.discoverFlags(intent);
if (flags.size() == 0) {
flags.add("None");
}
} else {
flags = Collections.singletonList("None");
}
showDialogWithFlags(activity.mName.getShortClassName(), flags);
});
return view;
}
/**
* Shows a dialog with a list.
* @param shortClassName The activity name and title of the dialog.
* @param flags The flags to list.
*/
private void showDialogWithFlags(String shortClassName, List<String> flags) {
FragmentTransaction transaction = mActivity.getFragmentManager().beginTransaction();
IntentDialogFragment.newInstance(shortClassName, flags).show(transaction, "intentDialog");
}
@Override
public boolean isChildSelectable(int i, int i1) {
return true;
}
@Override
public long getGroupId(int group) {
return mTasks.mChildren.get(group).mTaskId;
}
@Override
public long getChildId(int group, int child) {
return ((Node) getChild(group, child)).mName.hashCode();
}
private static class TaskViewHolder {
TextView taskIdTextView, numActivitiesTextView;
ImageView indicatorImageView;
TaskViewHolder(View view) {
indicatorImageView = view.findViewById(R.id.group_indicator);
taskIdTextView = view.findViewById(R.id.task_id);
numActivitiesTextView = view.findViewById(R.id.num_activities);
}
}
private static class ActivityViewHolder {
TextView activityNameTextView, activityNumTextView;
ImageView labelImageView;
ImageButton intentButtonView;
ActivityViewHolder(View view) {
activityNameTextView = view.findViewById(R.id.activity_name);
activityNumTextView = view.findViewById(R.id.activity_label);
labelImageView = view.findViewById(R.id.color_label);
intentButtonView = view.findViewById(R.id.intent_button);
}
}
}

View File

@@ -0,0 +1,350 @@
/*
* 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.example.android.intentplayground;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Static utility functions to query intent and activity manifest flags.
*/
class FlagUtils {
private static Class<Intent> sIntentClass = Intent.class;
private static List<ActivityInfo> sActivityInfos = null;
private static Intent sIntent = new Intent();
static final String INTENT_FLAG_PREFIX = "FLAG_ACTIVITY";
private static final String ACTIVITY_INFO_FLAG_PREFIX = "FLAG";
/**
* Returns a String list of flags active on this intent.
* @param intent The intent on which to query flags.
* @return A list of flags active on this intent.
*/
public static List<String> discoverFlags(Intent intent) {
int flags = intent.getFlags();
return Arrays.stream(intent.getClass().getDeclaredFields()) // iterate over Intent members
.filter(f -> f.getName().startsWith(INTENT_FLAG_PREFIX)) // filter FLAG_ fields
.filter(f -> {
try {
return (flags & f.getInt(intent)) > 0;
} catch (IllegalAccessException e) {
// Should never happen, the fields we are reading are public
throw new RuntimeException(e);
}
}) // filter fields that are present in intent
.map(Field::getName) // map present Fields to their string names
.collect(Collectors.toList());
}
/**
* Returns a full list of flags available to be set on an intent.
* @return A string list of all intent flags.
*/
public static List<String> getIntentFlagsAsString() {
return Arrays.stream(sIntentClass.getDeclaredFields())
.filter(f -> f.getName().startsWith(INTENT_FLAG_PREFIX))
.map(Field::getName)
.collect(Collectors.toList());
}
/**
* Get all defined {@link IntentFlag}s.
* @return All defined IntentFlags.
*/
public static List<IntentFlag> getAllIntentFlags() {
return Arrays.asList(IntentFlag.values());
}
/**
* Get intent flags by category/
* @return List of string flags (value) organized by category/function (key).
*/
public static Map<String, List<String>> intentFlagsByCategory() {
Map<String, List<String>> categories = new HashMap<>();
List<String> allFlags = getIntentFlagsAsString();
List<String> nonUser = new LinkedList<>();
List<String> recentsAndUi = new LinkedList<>();
List<String> newTask = new LinkedList<>();
List<String> clearTask = new LinkedList<>();
List<String> rearrangeTask = new LinkedList<>();
List<String> other = new LinkedList<>();
allFlags.forEach(flag -> {
if (hasSuffix(flag, "BROUGHT_TO_FRONT", "LAUNCHED_FROM_HISTORY")) {
nonUser.add(flag);
} else if (hasSuffix(flag, "RECENTS", "LAUNCH_ADJACENT", "NO_ANIMATION", "NO_HISTORY",
"RETAIN_IN_RECENTS")) {
recentsAndUi.add(flag);
} else if (hasSuffix(flag, "MULTIPLE_TASK", "NEW_TASK", "NEW_DOCUMENT",
"RESET_TASK_IF_NEEDED")) {
newTask.add(flag);
} else if (hasSuffix(flag, "CLEAR_TASK", "CLEAR_TOP", "CLEAR_WHEN_TASK_RESET")) {
clearTask.add(flag);
} else if (hasSuffix(flag, "REORDER_TO_FRONT", "SINGLE_TOP", "TASK_ON_HOME")) {
rearrangeTask.add(flag);
} else {
other.add(flag);
}
});
categories.put("Non-user", nonUser);
categories.put("Recents and UI", recentsAndUi);
categories.put("New Task", newTask);
categories.put("Clear Task", clearTask);
categories.put("Rearrange Task", rearrangeTask);
categories.put("Other", other);
return categories;
}
/**
* Checks the target string for any of the listed suffixes.
* @param target The string to test for suffixes.
* @param suffixes The suffixes to test the string for.
* @return True if the target string has any of the suffixes, false if not.
*/
private static boolean hasSuffix(String target, String... suffixes) {
for (String suffix: suffixes) {
if (target.endsWith(suffix)) {
return true;
}
}
return false;
}
/**
* Gets the integer value of an intent flag.
* @param flagName The field name of the flag.
*/
public static int flagValue(String flagName) {
try {
return sIntentClass.getField(flagName).getInt(sIntent);
} catch (Exception e) {
return 0;
}
}
/**
* Checks if the passed intent has the specified flag.
* @param intent The intent of which to examine the flags.
* @param flagName The string name of the intent flag to check for.
* @return True if the flag is present, false if not.
*/
public static boolean hasIntentFlag(Intent intent, String flagName) {
return (intent.getFlags() & flagValue(flagName)) > 0;
}
/**
* Checks if the passed intent has the specified flag.
* @param intent The intent of which to examine the flags.
* @param flag The corresponding enum {@link IntentFlag} of the intent flag to check for.
* @return True if the flag is present, false if not.
*/
public static boolean hasIntentFlag(Intent intent, IntentFlag flag) {
return hasIntentFlag(intent, flag.getName());
}
/**
* Checks if the passed activity has the specified flag set in its manifest.
* @param context A context from this application (used to access {@link PackageManager}.
* @param activity The activity of which to examine the flags.
* @param flag The corresponding enum {@link ActivityFlag} of the activity flag to check for.
* @return True if the flag is present, false if not.
*/
public static boolean hasActivityFlag(Context context, ComponentName activity,
ActivityFlag flag) {
return getActivityFlags(context, activity).contains(flag);
}
/**
* Returns an {@link EnumSet} of {@link ActivityFlag} corresponding to activity manifest flags
* activity on the specified activity.
* @param context A context from this application (used to access {@link PackageManager}.
* @param activity The activity of which to examine the flags.
* @return A set of ActivityFlags corresponding to activity manifest flags.
*/
public static EnumSet<ActivityFlag> getActivityFlags(Context context, ComponentName activity) {
loadActivityInfo(context);
EnumSet<ActivityFlag> flags = EnumSet.noneOf(ActivityFlag.class);
Optional<ActivityInfo> infoOptional = sActivityInfos.stream()
.filter(i-> i.name.equals(activity.getClassName()))
.findFirst();
if (!infoOptional.isPresent()) {
return flags;
}
ActivityInfo info = infoOptional.get();
if ((info.flags & ActivityInfo.FLAG_CLEAR_TASK_ON_LAUNCH) > 0) {
flags.add(ActivityFlag.CLEAR_TASK_ON_LAUNCH);
}
if ((info.flags & ActivityInfo.FLAG_ALLOW_TASK_REPARENTING) > 0) {
flags.add(ActivityFlag.ALLOW_TASK_REPARENTING);
}
switch (info.launchMode) {
case ActivityInfo.LAUNCH_SINGLE_INSTANCE:
flags.add(ActivityFlag.LAUNCH_MODE_SINGLE_INSTANCE);
break;
case ActivityInfo.LAUNCH_SINGLE_TASK:
flags.add(ActivityFlag.LAUNCH_MODE_SINGLE_TASK);
break;
case ActivityInfo.LAUNCH_SINGLE_TOP:
flags.add(ActivityFlag.LAUNCH_MODE_SINGLE_TOP);
break;
case ActivityInfo.LAUNCH_MULTIPLE:
default:
flags.add(ActivityFlag.LAUNCH_MODE_STANDARD);
break;
}
switch(info.documentLaunchMode) {
case ActivityInfo.DOCUMENT_LAUNCH_INTO_EXISTING:
flags.add(ActivityFlag.DOCUMENT_LAUNCH_MODE_INTO_EXISTING);
break;
case ActivityInfo.DOCUMENT_LAUNCH_ALWAYS:
flags.add(ActivityFlag.DOCUMENT_LAUNCH_MODE_ALWAYS);
break;
case ActivityInfo.DOCUMENT_LAUNCH_NEVER:
flags.add(ActivityFlag.DOCUMENT_LAUNCH_MODE_NEVER);
break;
case ActivityInfo.DOCUMENT_LAUNCH_NONE:
default:
flags.add(ActivityFlag.DOCUMENT_LAUNCH_MODE_NONE);
break;
}
return flags;
}
private static void loadActivityInfo(Context context) {
if (sActivityInfos == null) {
PackageInfo packInfo;
// Retrieve activities and their manifest flags
PackageManager pm = context.getPackageManager();
try {
packInfo = pm.getPackageInfo(context.getPackageName(),
PackageManager.GET_ACTIVITIES);
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
sActivityInfos = Arrays.asList(packInfo.activities);
}
}
/**
* Discover which flags on the specified {@link ActivityInfo} are enabled,
* and return them as a list of strings.
* @param activity The activity from which you want to find flags.
* @return A list of flags.
*/
public static List<String> getActivityFlags(ActivityInfo activity) {
int flags = activity.flags;
List<String> flagStrings = Arrays.stream(activity.getClass().getDeclaredFields())
.filter(f -> f.getName().startsWith(ACTIVITY_INFO_FLAG_PREFIX))
.filter(f -> {
try {
return (flags & f.getInt(activity)) > 0;
} catch (IllegalAccessException e) {
// Should never happen, the fields we are reading are public
throw new RuntimeException(e);
}
}) // filter fields that are present in intent
.map(Field::getName) // map present Fields to their string names
.map(name -> camelify(name.substring(ACTIVITY_INFO_FLAG_PREFIX.length())))
.map(s -> s.concat("=true"))
.collect(Collectors.toList());
// check for launchMode
if (activity.launchMode != 0) {
String lm = "launchMode=";
switch(activity.launchMode) {
case ActivityInfo.LAUNCH_SINGLE_INSTANCE:
lm += "singleInstance";
break;
case ActivityInfo.LAUNCH_SINGLE_TASK:
lm += "singleTask";
break;
case ActivityInfo.LAUNCH_SINGLE_TOP:
lm += "singleTop";
break;
case ActivityInfo.LAUNCH_MULTIPLE:
default:
lm += "standard";
break;
}
flagStrings.add(lm);
}
// check for documentLaunchMode
if (activity.documentLaunchMode != 0) {
String dlm = "documentLaunchMode=";
switch(activity.documentLaunchMode) {
case ActivityInfo.DOCUMENT_LAUNCH_INTO_EXISTING:
dlm += "intoExisting";
break;
case ActivityInfo.DOCUMENT_LAUNCH_ALWAYS:
dlm += "always";
break;
case ActivityInfo.DOCUMENT_LAUNCH_NEVER:
dlm += "never";
break;
case ActivityInfo.DOCUMENT_LAUNCH_NONE:
default:
dlm += "none";
break;
}
flagStrings.add(dlm);
}
if (activity.taskAffinity != null) {
flagStrings.add("taskAffinity="+ activity.taskAffinity);
}
return flagStrings;
}
/**
* Takes a snake_case and converts to CamelCase.
* @param snake A snake_case string.
* @return A camelified string.
*/
public static String camelify(String snake) {
List<String> words = Arrays.asList(snake.split("_"));
StringBuilder output = new StringBuilder(words.get(0).toLowerCase());
words.subList(1,words.size()).forEach(s -> {
String first = s.substring(0,1).toUpperCase();
String rest = s.substring(1).toLowerCase();
output.append(first).append(rest);
});
return output.toString();
}
/**
* Retrieves the corresponding enum {@link IntentFlag} for the string flag.
* @param stringFlag the name of the intent flag.
* @return The corresponding IntentFlag.
*/
public static IntentFlag getFlagForString(String stringFlag) {
return getAllIntentFlags().stream().filter(flag -> flag.getName().equals(stringFlag)).findAny()
.orElse(null);
}
}

View File

@@ -0,0 +1,338 @@
/*
* 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.example.android.intentplayground;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.support.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.Toast;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Displays options to build an intent with different configurations of flags
* and target activities, and allows the user to launch an activity with the built intent.
*/
public class IntentBuilderView extends FrameLayout implements View.OnClickListener,
CompoundButton.OnCheckedChangeListener {
protected final int TAG_FLAG = R.id.tag_flag;
protected final int TAG_SUGGESTED = R.id.tag_suggested;
protected ComponentName mActivityToLaunch;
private boolean mVerifyMode;
private ColorStateList mSuggestTint;
private ColorStateList mDefaultTint;
private LinearLayout mLayout;
private Context mContext;
private LayoutInflater mInflater;
private OnLaunchCallback mLaunchCallback;
/**
* Constructs a new IntentBuilderView, in the specified mode.
* @param context The context of the activity that holds this view.
* @param mode The mode to launch in (if null, default mode turns suggestions off). Passing
* {@link com.example.android.intentplayground.BaseActivity.Mode} will turn on suggestions
* by default.
*/
public IntentBuilderView(@NonNull Context context, BaseActivity.Mode mode) {
super(context);
mContext = context;
mInflater = LayoutInflater.from(context);
mLayout = (LinearLayout) mInflater.inflate(R.layout.view_build_intent,
this /* root */, false /* attachToRoot */);
addView(mLayout, new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));
mActivityToLaunch = new ComponentName(context,
TaskAffinity1Activity.class);
mSuggestTint = context.getColorStateList(R.color.suggested_checkbox);
mDefaultTint = context.getColorStateList(R.color.default_checkbox);
mVerifyMode = mode != null && mode == BaseActivity.Mode.VERIFY;
setTag(BaseActivity.BUILDER_VIEW);
setId(R.id.build_intent_container);
setBackground(context.getResources().getDrawable(R.drawable.card_background,
null /*theme*/));
setupViews();
}
private Class<?> getClass(String name) {
String fullName = mContext.getPackageName().concat(".").concat(name);
try {
return Class.forName(fullName);
} catch (ClassNotFoundException e) {
if (BuildConfig.DEBUG) e.printStackTrace();
throw new RuntimeException(e);
}
}
private void setupViews() {
PackageInfo packInfo;
// Retrieve activities and their manifest flags
PackageManager pm = mContext.getPackageManager();
try {
packInfo = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
} catch (PackageManager.NameNotFoundException e) {
Toast.makeText(mContext,
"Cannot find activities, this should never happen " + e.toString(),
Toast.LENGTH_SHORT).show();
throw new RuntimeException(e);
}
List<ActivityInfo> activities = Arrays.asList(packInfo.activities);
Map<ActivityInfo, List<String>> activityToFlags = new HashMap<>();
activities.forEach(activityInfo ->
activityToFlags.put(activityInfo, FlagUtils.getActivityFlags(activityInfo)));
// Get handles to views
LinearLayout flagBuilderLayout = mLayout.findViewById(R.id.build_intent_flags);
RadioGroup activityRadios = mLayout.findViewById(R.id.radioGroup_launchMode);
// Populate views with text
fillCheckBoxLayout(flagBuilderLayout, FlagUtils.intentFlagsByCategory(),
R.layout.section_header, R.id.header_title, R.layout.checkbox_list_item,
R.id.checkBox_item);
// Add radios for activity combos
activityToFlags.forEach((activityInfo, manifestFlags) -> {
LinearLayout actRadio = (LinearLayout) mInflater
.inflate(R.layout.activity_radio_list_item, null /* root */);
RadioButton rb = actRadio.findViewById(R.id.radio_launchMode);
rb.setText(activityInfo.name.substring(activityInfo.name.lastIndexOf('.') + 1));
rb.setTag(activityInfo);
((TextView) actRadio.findViewById(R.id.activity_desc)).setText(
manifestFlags.stream().collect(Collectors.joining("\n")));
rb.setOnClickListener(this);
activityRadios.addView(actRadio);
});
mLayout.findViewById(R.id.launch_button).setOnClickListener(this);
((CompoundButton) mLayout.findViewById(R.id.suggestion_switch))
.setOnCheckedChangeListener(this);
}
/**
* Fills the {@link ViewGroup} with a list separated by section
* @param layout The layout to fill
* @param categories A map of category names to list items within that category
* @param categoryLayoutRes the layout resource of the category header view
* @param categoryViewId the resource id of the category {@link TextView} within the layout
* @param itemLayoutRes the layout resource of the list item view
* @param itemViewId the resource id of the item {@link TextView} within the item layout
*/
private void fillCheckBoxLayout(ViewGroup layout, Map<String, List<String>> categories,
int categoryLayoutRes, int categoryViewId, int itemLayoutRes,int itemViewId) {
layout.removeAllViews();
for (String category : categories.keySet()) {
View categoryLayout = mInflater.inflate(categoryLayoutRes, layout,
false /* attachToRoot */);
TextView categoryView = categoryLayout.findViewById(categoryViewId);
categoryView.setText(category);
layout.addView(categoryLayout);
for (String item : categories.get(category)) {
View itemLayout = mInflater.inflate(itemLayoutRes, layout,
false /* attachToRoot */);
CheckBox itemView = itemLayout.findViewById(itemViewId);
IntentFlag flag = FlagUtils.getFlagForString(item);
itemView.setTag(TAG_FLAG, flag);
itemView.setText(item);
itemView.setOnCheckedChangeListener(this);
layout.addView(itemLayout);
}
}
}
@Override
public void onClick(View view) {
// Handles selection of target activity
if (view instanceof RadioButton) {
ActivityInfo tag = (ActivityInfo) view.getTag();
mActivityToLaunch = new ComponentName(mContext,
getClass(tag.name.substring(tag.name.lastIndexOf(".") + 1)));
} else if (view instanceof Button && view.getId() == R.id.launch_button) {
// Handles click on Launch Button
mLaunchCallback.launchActivity(currentIntent());
}
}
private Intent currentIntent() {
LinearLayout flagBuilder = mLayout.findViewById(R.id.build_intent_flags);
Intent intent = new Intent();
// Gather flags from flag builder checkbox list
childrenOfGroup(flagBuilder, CheckBox.class)
.forEach(checkbox -> {
int flagVal = FlagUtils.flagValue(checkbox.getText().toString());
if (checkbox.isChecked()) {
intent.addFlags(flagVal);
} else {
intent.removeFlags(flagVal);
}
});
intent.setComponent(mActivityToLaunch);
return intent;
}
public void setOnLaunchCallback(OnLaunchCallback listener) {
mLaunchCallback = listener;
}
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
int buttonId = compoundButton.getId();
if (buttonId == R.id.checkBox_item) {
// A checkbox was checked/unchecked
IntentFlag flag = (IntentFlag) compoundButton.getTag(TAG_FLAG);
if (flag != null && mVerifyMode) {
refreshConstraints();
if (checked) {
suggestFlags(flag);
selectFlags(flag.getRequests());
} else {
clearSuggestions();
}
}
} else if (buttonId == R.id.suggestion_switch) {
// Suggestions were turned on/off
clearSuggestions();
mVerifyMode = checked;
if (mVerifyMode) {
refreshConstraints();
getCheckedFlags().forEach(this::suggestFlags);
} else enableAllFlags();
}
}
private void refreshConstraints() {
enableAllFlags();
getCheckedFlags().forEach(flag -> disableFlags(flag.getConflicts()));
}
private void suggestFlags(IntentFlag flag) {
clearSuggestions();
List<String> suggestions = flag.getComplements().stream().map(IntentFlag::getName)
.collect(Collectors.toList());
getAllCheckBoxes().stream().filter(box ->
suggestions.contains(((IntentFlag) box.getTag(TAG_FLAG)).getName()))
.forEach(box -> {
box.setButtonTintList(mSuggestTint);
box.setTag(TAG_SUGGESTED, true);
});
}
private void clearSuggestions() {
getAllCheckBoxes().forEach(box -> box.setButtonTintList(mDefaultTint));
}
/**
* Clears all of the checkboxes in this builder.
*/
public void clearFlags() {
getAllCheckBoxes().forEach(box -> box.setChecked(false));
}
private List<CheckBox> getAllCheckBoxes() {
View layout = mLayout;
ViewGroup flagBuilder = (LinearLayout) layout.findViewById(R.id.build_intent_flags);
List<CheckBox> checkBoxes = new LinkedList<>();
for (int i = 0; i < flagBuilder.getChildCount(); i++) {
View child = flagBuilder.getChildAt(i);
if (child instanceof CheckBox) {
checkBoxes.add((CheckBox) child);
}
}
return checkBoxes;
}
/**
* Retrieve children of a certain type from a {@link ViewGroup}.
* @param group the ViewGroup to retrieve children from.
*/
protected static <T> List<T> childrenOfGroup(ViewGroup group, Class<T> viewType) {
List<T> list = new LinkedList<>();
for (int i = 0; i < group.getChildCount(); i++) {
View v = group.getChildAt(i);
if (viewType.isAssignableFrom(v.getClass())) list.add(viewType.cast(v));
}
return list;
}
/**
* Selects the checkboxes for the given list of flags.
* @param flags A list of mIntent flags to select.
*/
public void selectFlags(List<String> flags) {
getAllCheckBoxes().forEach(box -> {
if (flags.contains(box.getText())) {
box.setChecked(true);
}
});
}
/**
* Selects the checkboxes for the given list of flags.
* @param flags A list of mIntent flags to select.
*/
public void selectFlags(Collection<IntentFlag> flags) {
selectFlags(flags.stream().map(IntentFlag::getName).collect(Collectors.toList()));
}
private void enableAllFlags() {
getAllCheckBoxes().forEach(box -> box.setEnabled(true));
}
private Collection<CheckBox> getChecked() {
return getAllCheckBoxes().stream().filter(CompoundButton::isChecked)
.collect(Collectors.toList());
}
private Collection<IntentFlag> getCheckedFlags() {
return getChecked().stream().map(checkBox -> (IntentFlag) checkBox.getTag(TAG_FLAG))
.collect(Collectors.toList());
}
private void disableFlags(Collection<IntentFlag> flags) {
flags.forEach(flag -> getCheckBox(flag).setEnabled(false));
}
private CheckBox getCheckBox(IntentFlag flag) {
return getAllCheckBoxes().stream().filter(box -> flag.getName().equals(box.getText()))
.findFirst().orElse(null);
}
/**
* A functional interface that represents the action to take upon the user pressing the launch
* button within this view.
*/
public interface OnLaunchCallback {
void launchActivity(Intent intent);
}
}

View File

@@ -0,0 +1,96 @@
/*
* 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.example.android.intentplayground;
import android.app.DialogFragment;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ScrollView;
import java.util.ArrayList;
import java.util.List;
/**
* Shows a dialog with an activity name and a list of intent flags.
*/
public class IntentDialogFragment extends DialogFragment {
private List<String> mFlags;
private String mActivityName;
private static final String ARGUMENT_ACTIVITY_NAME = "activityName";
private static final String ARGUMENT_FLAGS = "flags";
/**
* Creates a new IntentDialogFragment to display the given flags.
* @param activityName The name of the activity, also the title of the dialog.
* @param flags The list of flags to be displayed.
* @return A new IntentDialogFragment.
*/
public static IntentDialogFragment newInstance(String activityName, List<String> flags) {
IntentDialogFragment fragment = new IntentDialogFragment();
Bundle args = new Bundle();
args.putString(ARGUMENT_ACTIVITY_NAME, activityName);
args.putStringArrayList(ARGUMENT_FLAGS, new ArrayList<>(flags));
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
mFlags = args.getStringArrayList(ARGUMENT_FLAGS);
mActivityName = args.getString(ARGUMENT_ACTIVITY_NAME);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
Bundle savedInstanceState) {
getDialog().setTitle(mActivityName + getString(R.string.dialog_intent_flags));
LinearLayout rootLayout = (LinearLayout) inflater
.inflate(R.layout.fragment_intent_dialog, container, false /* attachToRoot */);
ListView flagsListView = rootLayout.findViewById(R.id.flag_list);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(getActivity(),
R.layout.dialog_list_item, R.id.item, mFlags);
flagsListView.setAdapter(adapter);
rootLayout.findViewById(R.id.dialog_cancel).setOnClickListener(view -> {
getDialog().dismiss();
});
Button copyFlagsButton = rootLayout.findViewById(R.id.copy_flags_button);
if (mFlags.get(0).equals("None")) {
copyFlagsButton.setEnabled(false);
} else {
copyFlagsButton.setOnClickListener(view -> {
IntentBuilderView intentBuilderView = getActivity()
.findViewById(R.id.root_container)
.findViewWithTag(BaseActivity.BUILDER_VIEW);
intentBuilderView.selectFlags(mFlags);
getDialog().dismiss();
((ScrollView) getActivity().findViewById(R.id.scroll_container)).smoothScrollTo(0,
Float.valueOf(intentBuilderView.getY()).intValue());
});
}
return rootLayout;
}
}

View File

@@ -0,0 +1,102 @@
/*
* 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.example.android.intentplayground;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import static java.util.Collections.emptySet;
/**
* Represents the different intent flags related to activities and tasks.
*/
enum IntentFlag {
SINGLE_TOP ("FLAG_ACTIVITY_SINGLE_TOP", emptySet(), emptySet(), emptySet()),
BROUGHT_TO_FRONT ("FLAG_ACTIVITY_BROUGHT_TO_FRONT", emptySet(), emptySet(), emptySet()),
NEW_TASK ("FLAG_ACTIVITY_NEW_TASK", emptySet(), emptySet(), emptySet()),
CLEAR_TASK ("FLAG_ACTIVITY_CLEAR_TASK", emptySet(), emptySet(), setOf(NEW_TASK)),
CLEAR_TOP ("FLAG_ACTIVITY_CLEAR_TOP", setOf(SINGLE_TOP, NEW_TASK), emptySet(), emptySet()),
MULTIPLE_TASK ("FLAG_ACTIVITY_MULTIPLE_TASK", emptySet(), emptySet(), setOf(NEW_TASK)),
NEW_DOCUMENT ("FLAG_ACTIVITY_NEW_DOCUMENT", setOf(MULTIPLE_TASK), emptySet(),
emptySet()),
RETAIN_IN_RECENTS ("FLAG_ACTIVITY_RETAIN_IN_RECENTS", setOf(NEW_DOCUMENT),
emptySet(), emptySet()),
CLEAR_WHEN_TASK_RESET ("FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET", emptySet(), emptySet(),
emptySet()),
EXCLUDE_FROM_RECENTS ("FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS", emptySet(), setOf(RETAIN_IN_RECENTS),
emptySet()),
FORWARD_RESULT ("FLAG_ACTIVITY_FORWARD_RESULT", emptySet(), emptySet(), emptySet()),
LAUNCHED_FROM_HISTORY ("FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY", emptySet(), emptySet(),
emptySet()),
LAUNCH_ADJACENT ("FLAG_ACTIVITY_LAUNCH_ADJACENT", setOf(MULTIPLE_TASK), emptySet(),
setOf(NEW_TASK)),
NO_ANIMATION ("FLAG_ACTIVITY_NO_ANIMATION", emptySet(), emptySet(), emptySet()),
NO_HISTORY ("FLAG_ACTIVITY_NO_HISTORY", emptySet(), emptySet(), emptySet()),
NO_USER_ACTION ("FLAG_ACTIVITY_NO_USER_ACTION", emptySet(), emptySet(), emptySet()),
PREVIOUS_IS_TOP ("FLAG_ACTIVITY_PREVIOUS_IS_TOP", emptySet(), emptySet(), emptySet()),
REORDER_TO_FRONT ("FLAG_ACTIVITY_REORDER_TO_FRONT", emptySet(), setOf(CLEAR_TOP), emptySet()),
RESET_TASK_IF_NEEDED ("FLAG_ACTIVITY_RESET_TASK_IF_NEEDED", emptySet(), emptySet(), emptySet()),
TASK_ON_HOME ("FLAG_ACTIVITY_TASK_ON_HOME", emptySet(), emptySet(), setOf(NEW_TASK));
public String name;
private Set<IntentFlag> mComplements = new HashSet<>();
private Set<IntentFlag> mConflicts = new HashSet<>();
private Set<IntentFlag> mRequests = new HashSet<>();
IntentFlag(String name, Set<IntentFlag> complements, Set<IntentFlag> conflicts,
Set<IntentFlag> requests) {
this.name = name;
this.mComplements = complements;
this.mConflicts = conflicts;
this.mRequests = requests;
}
/**
* @return A set of flags that complement the action of this flag.
*/
public Set<IntentFlag> getComplements() {
return mComplements;
}
/**
* @return A set of flags that conflict with the action of this flag.
*/
public Set<IntentFlag> getConflicts() {
return mConflicts;
}
/**
* @return A set of flags that are necessary for the action of this flag.
*/
public Set<IntentFlag> getRequests() {
return mRequests;
}
public String getName() {
return name;
}
/**
* Convenience method to create a set of intent flags.
*/
protected static Set<IntentFlag> setOf(IntentFlag... flags) {
return Arrays.stream(flags).collect(Collectors.toSet());
}
}

View File

@@ -0,0 +1,97 @@
/*
* 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.example.android.intentplayground;
import android.app.Activity;
import android.app.Fragment;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.List;
import java.util.Set;
/**
* Displays details about the intent that launched the current activity.
*/
public class IntentFragment extends Fragment {
private TextView mActionTextView, mUriTextView, mTypeTextView, mPackageTextView;
private LinearLayout mCategoryListLayout, mFlagListLayout;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
Bundle savedInstanceState) {
LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.fragment_intent, container,
false /* attachToRoot */);
// Get handles to views
mActionTextView = layout.findViewById(R.id.intentAction);
mUriTextView = layout.findViewById(R.id.intentUri);
mTypeTextView = layout.findViewById(R.id.intentType);
mPackageTextView = layout.findViewById(R.id.intentPackage);
mCategoryListLayout = layout.findViewById(R.id.intentCategories);
mFlagListLayout = layout.findViewById(R.id.intentFlags);
return layout;
}
@Override
public void onResume() {
super.onResume();
Activity activity = getActivity();
Intent intent = activity.getIntent();
// Get intent info
String action = intent.getAction();
String dataUri = intent.getDataString();
String intentType = intent.getType();
String intentPackage = intent.getPackage();
Set<String> categories = intent.getCategories();
List<String> flags = FlagUtils.discoverFlags(intent);
// set data
mActionTextView.setText(action);
mUriTextView.setText(dataUri);
mTypeTextView.setText(intentType);
mPackageTextView.setText(intentPackage);
if (categories != null) {
ArrayAdapter<String> categoryAdapter = new ArrayAdapter<>(activity,
R.layout.simple_list_item,
categories.toArray(new String[0]));
fillLayout(mCategoryListLayout, categoryAdapter);
}
ArrayAdapter<String> flagAdapter = new ArrayAdapter<>(activity,
R.layout.simple_list_item, flags);
fillLayout(mFlagListLayout, flagAdapter);
}
/**
* Takes a @{link ViewGroup} and uses the given adapter to fill it; used in the cases where we
* need a non-scrollable list that is a child of {@link android.widget.ScrollView}.
* @param layout The layout to be filled.
* @param adapter The adapter that provides the views for the layout.
*/
private void fillLayout(ViewGroup layout, ArrayAdapter<?> adapter) {
layout.removeAllViews();
for (int i = 0; i < adapter.getCount(); i++) {
layout.addView(adapter.getView(i, null /* convertView */, null /* parent */));
}
}
}

View File

@@ -16,49 +16,186 @@
package com.example.android.intentplayground;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.content.ComponentName;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.widget.FrameLayout;
import android.widget.TextView;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
import static com.example.android.intentplayground.Node.newTaskNode;
/**
* A singleInstance activity that is responsible for a launching a bootstrap stack of activities
* A singleInstance activity that is responsible for a launching a bootstrap stack of activities.
*/
public class LauncherActivity extends BaseActivity {
private TestBase mTester;
public static final String TAG = "LauncherActivity";
private static final long SNACKBAR_DELAY = 75;
private TestBase mTester;
private boolean mFirstLaunch;
private View mSnackBarRootView;
private boolean mSnackBarIsVisible = false;
private boolean mDontLaunch = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Node mRoot = new Node(new ComponentName(this, LauncherActivity.class));
// Describe initial setup of tasks
// create singleTask, singleInstance, and two documents in separate tasks
mRoot.addChild( new Node(new ComponentName(this, SingleTaskActivity.class)))
.addChild( new Node(new ComponentName(this, DocumentLaunchAlwaysActivity.class)))
.addChild( new Node(new ComponentName(this, DocumentLaunchIntoActivity.class)));
// Create three tasks with three activities each, with affinity set
Node taskAffinity1 = new Node(new ComponentName(this, TaskAffinity1Activity.class));
taskAffinity1
.addChild(new Node(new ComponentName(this, TaskAffinity1Activity.class)))
.addChild(new Node(new ComponentName(this, TaskAffinity1Activity.class)));
Node taskAffinity2 = new Node(new ComponentName(this, ClearTaskOnLaunchActivity.class));
taskAffinity2
.addChild(new Node(new ComponentName(this, TaskAffinity2Activity.class)))
.addChild(new Node(new ComponentName(this, TaskAffinity2Activity.class)));
Node taskAffinity3 = new Node(new ComponentName(this, TaskAffinity3Activity.class));
taskAffinity3
.addChild(new Node(new ComponentName(this, TaskAffinity3Activity.class)))
.addChild(new Node(new ComponentName(this, TaskAffinity3Activity.class)));
mRoot.addChild(taskAffinity1).addChild(taskAffinity2).addChild(taskAffinity3);
mTester = new TestBase(this, mRoot);
mTester.setupActivities(TestBase.LaunchStyle.TASK_STACK_BUILDER);
setupTaskPreset();
mFirstLaunch = true;
}
/**
* Launches activity with the selected options
* Sets up a hierarchy of{@link Node}s that represents the desired initial task stack.
*/
public void launchActivity(Intent customIntent) {
customIntent.putExtra(TestBase.EXPECTED_HIERARCHY, mTester.computeExpected(customIntent));
startActivity(customIntent);
protected void setupTaskPreset() {
Node mRoot = Node.newRootNode();
// Describe initial setup of tasks
// create singleTask, singleInstance, and two documents in separate tasks
Node singleTask = newTaskNode()
.addChild( new Node(new ComponentName(this, SingleTaskActivity.class)));
Node docLaunchAlways = newTaskNode()
.addChild( new Node(new ComponentName(this, DocumentLaunchAlwaysActivity.class)));
Node docLaunchInto = newTaskNode()
.addChild( new Node(new ComponentName(this, DocumentLaunchIntoActivity.class)));
// Create three t0asks with three activities each, with affinity set
Node taskAffinity1 = newTaskNode()
.addChild(new Node(new ComponentName(this, TaskAffinity1Activity.class)))
.addChild(new Node(new ComponentName(this, TaskAffinity1Activity.class)))
.addChild(new Node(new ComponentName(this, TaskAffinity1Activity.class)));
Node taskAffinity2 = newTaskNode()
.addChild(new Node(new ComponentName(this, TaskAffinity2Activity.class)))
.addChild(new Node(new ComponentName(this, TaskAffinity2Activity.class)))
.addChild(new Node(new ComponentName(this, TaskAffinity2Activity.class)));
Node taskAffinity3 = newTaskNode()
.addChild(new Node(new ComponentName(this, TaskAffinity3Activity.class)))
.addChild(new Node(new ComponentName(this, TaskAffinity3Activity.class)))
.addChild(new Node(new ComponentName(this, TaskAffinity3Activity.class)));
mRoot.addChild(singleTask).addChild(docLaunchAlways).addChild(docLaunchInto)
.addChild(taskAffinity1).addChild(taskAffinity2).addChild(taskAffinity3);
mTester = new TestBase(this, mRoot);
}
@Override
protected void loadMode(Mode mode) {
if (mode == Mode.LAUNCH) {
super.loadMode(mode);
long nonEmptyTasks = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
nonEmptyTasks = getSystemService(ActivityManager.class)
.getAppTasks().stream().filter(t -> TaskInfo.getActivities(t.getTaskInfo()).size() != 0)
.count();
}
if (nonEmptyTasks <= 1 && !mDontLaunch) askToLaunchTasks();
}
}
@Override
protected void onResume() {
super.onResume();
if (!mFirstLaunch && mSnackBarIsVisible) {
hideSnackBar();
} else {
mFirstLaunch = false;
}
}
private void askToLaunchTasks() {
AlertDialog dialog = new AlertDialog.Builder(this)
.setMessage(R.string.launch_explanation)
.setTitle(R.string.ask_to_launch)
.setPositiveButton(R.string.ask_to_launch_affirm, (dialogInterface, i) -> {
showSnackBar(() -> {
mTester.startActivities(TestBase.LaunchStyle.TASK_STACK_BUILDER);
});
dialogInterface.dismiss();
})
.setNegativeButton(R.string.ask_to_launch_cancel, (dialogInterface, i) -> {
dialogInterface.dismiss();
mDontLaunch = true;
hideSnackBar();
})
.create();
startSnackBar();
dialog.show();
}
/**
* Prepares the custom snackbar window. We must use an application overlay because normal
* toasts and snackbars disappear once TestBase.startActivities() starts.
*/
private void startSnackBar() {
WindowManager wm = getSystemService(WindowManager.class);
LayoutInflater inflater = getLayoutInflater();
FrameLayout frame = new FrameLayout(this);
inflater.inflate(R.layout.snack_loading, frame, true /* attachToRoot */);
mSnackBarRootView = frame;
if (!requestOverlayPermission()) return;
wm.addView(frame, makeLayoutParams(TYPE_APPLICATION_OVERLAY));
mSnackBarIsVisible = true;
}
private boolean requestOverlayPermission() {
if (Settings.canDrawOverlays(this)) {
return true;
} else {
// Start manage overlays activity
Intent intent = new Intent().setAction(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
.setData(Uri.fromParts(getString(R.string.package_uri_scheme),
getPackageName(), null /* fragment */));
startActivity(intent);
return false;
}
}
private void showSnackBar(Runnable onShow) {
TextView tv = mSnackBarRootView.findViewById(R.id.snackbar_text);
tv.setText(getString(R.string.launch_wait_toast));
mSnackBarRootView.postDelayed(onShow, SNACKBAR_DELAY);
}
private void hideSnackBar() {
if (mSnackBarIsVisible) {
WindowManager wm = getSystemService(WindowManager.class);
mSnackBarIsVisible = false;
wm.removeView(mSnackBarRootView);
}
}
private LayoutParams makeLayoutParams(int type) {
LayoutParams params = new LayoutParams(type, FLAG_NOT_TOUCH_MODAL);
params.format = PixelFormat.TRANSLUCENT;
params.width = MATCH_PARENT;
params.height = WRAP_CONTENT;
params.gravity = Gravity.getAbsoluteGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM,
View.LAYOUT_DIRECTION_RTL);
return params;
}
/**
* Launches activity with the selected options.
*/
@Override
public void launchActivity(Intent intent) {
startActivityForResult(intent, LAUNCH_REQUEST_CODE);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
}
}

View File

@@ -17,6 +17,6 @@
package com.example.android.intentplayground;
/**
* An activity with noHistory="true" set in AndroidManifest.xml
* An activity with noHistory="true" set in AndroidManifest.xml.
*/
public class NoHistoryActivity extends BaseActivity {}

View File

@@ -0,0 +1,276 @@
/*
* 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.example.android.intentplayground;
import android.content.ComponentName;
import android.content.Intent;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* This class represents a node in the tree of tasks. It can either represent a task
* or an activity.
*/
public class Node implements Parcelable, Comparable<Node> {
static final int NEW_TASK_ID = 0xa4d701d;
public static final int ROOT_NODE_ID = 0xAABBCCDD;
public int mTaskId;
public List<Node> mChildren = new LinkedList<>();
public ComponentName mName;
private static final int CURRENT = 0x1;
private static final int MODIFIED = 0x2;
private static final int NEW = 0x4;
private boolean mIsTaskNode;
private int mOptionFlags;
private Intent mIntent;
Node(ComponentName data) {
mIsTaskNode = false;
mName = data;
}
/**
* Create a task Node.
* @param taskId the id of the task.
*/
Node(int taskId) {
mIsTaskNode = true;
mTaskId = taskId;
}
/**
* Creates a Node with the same data as the parameter (copy constructor).
* @param other Node to copy over.
*/
Node(Node other) {
if (other.mIsTaskNode) {
mIsTaskNode = true;
mTaskId = other.mTaskId;
} else {
mIsTaskNode = false;
mName = other.mName.clone();
}
mOptionFlags = other.mOptionFlags;
mIntent = other.mIntent;
other.mChildren.forEach(child -> addChild(new Node(child)));
}
/**
* Adds a child to this Node's children.
* @param child The child node to add.
* @return returns This Node object for method chaining.
*/
Node addChild(Node child) {
mChildren.add(child);
return this;
}
/**
* Adds a child to the beginning of the list of this Node's children.
* @param child The child node to add.
* @return This Node object for method chaining.
*/
Node addFirstChild(Node child) {
mChildren.add(0, child);
return this;
}
/**
* Clear children from this Node.
* @return returns This Node object for method chaining.
*/
Node clearChildren() {
mChildren.clear();
return this;
}
static Node newTaskNode() {
return new Node(NEW_TASK_ID);
}
static Node newRootNode() {
return new Node(ROOT_NODE_ID);
}
boolean isModified() {
return (mOptionFlags & MODIFIED) != 0;
}
void setModified(boolean value) {
if (value) {
mOptionFlags |= MODIFIED;
} else {
mOptionFlags &= ~MODIFIED;
}
}
boolean isNew() {
return ((mOptionFlags & NEW) != 0) || (mIsTaskNode && (mTaskId == NEW_TASK_ID));
}
void setNew(boolean value) {
if (value) {
mOptionFlags |= NEW;
} else {
mOptionFlags &= ~NEW;
}
}
boolean isCurrent() {
return (mOptionFlags & CURRENT) != 0;
}
Node setCurrent(boolean value) {
if (value) {
mOptionFlags |= CURRENT;
} else {
mOptionFlags &= ~CURRENT;
}
return this;
}
public Node setIntent(Intent intent) {
mIntent = new Intent(intent);
return this;
}
public Intent getIntent() {
return mIntent;
}
private Node(Parcel in) {
mIsTaskNode = in.readInt() == 1;
if (mIsTaskNode) {
mTaskId = in.readInt();
} else {
mName = ComponentName.CREATOR.createFromParcel(in);
}
if (in.readInt() > 0) {
in.readTypedList(mChildren, Node.CREATOR);
} else {
mChildren = new LinkedList<>();
}
mOptionFlags = in.readInt();
if (in.readInt() > 0) {
mIntent = Intent.CREATOR.createFromParcel(in);
}
}
/**
* Compare the tree represented by this Node to another to determine if
* they are isomorphic.
* @param other The Node to compare to this.
*/
public boolean equals(Node other) {
if (mIsTaskNode && other.mIsTaskNode) {
// Check if taskIds are equal, or if one is a new task (which is essentially a wildcard)
if ((mTaskId != other.mTaskId) && (mTaskId != NEW_TASK_ID)
&& (other.mTaskId != NEW_TASK_ID)) {
return false;
}
} else if (!mIsTaskNode && !other.mIsTaskNode){
if (!other.mName.equals(mName)) return false;
} else return false;
if (mChildren.size() == 0 && other.mChildren.size() == 0) {
return true;
} else if (mChildren.size() != other.mChildren.size()){
return false;
} else {
Collections.sort(mChildren);
Collections.sort(other.mChildren);
for (int i = 0; i < mChildren.size(); i++) {
if (!mChildren.get(i).equals(other.mChildren.get(i))) {
return false;
}
}
return true;
}
}
/**
* Note: this class has a natural ordering that is inconsistent with equals().
* compareTo() makes comparison based on the {@link ComponentName} that this class
* holds, and does not consider its children.
*/
public int compareTo(Node o) {
return mIsTaskNode ? Integer.valueOf(mTaskId).compareTo(o.mTaskId)
: mName.compareTo(o.mName);
}
@Override
public String toString() {
StringBuilder output = new StringBuilder("Node ");
if (isCurrent()) output.append("current ");
if (isNew()) output.append("new ");
if (isModified()) output.append("modified ");
output.append("<<");
if (mIsTaskNode) output.append("taskId=").append(mTaskId);
else output.append(mName.toShortString());
if (mIntent != null) {
output.append("intent:(");
FlagUtils.discoverFlags(mIntent).forEach(flag -> {
output.append(flag.replace(FlagUtils.INTENT_FLAG_PREFIX, "")).append(',');
});
output.append(")");
}
output.append(">> {");
if (!mChildren.isEmpty()) output.append('\n');
mChildren.forEach(child -> Arrays.asList(child.toString().split("\n")).forEach(line ->
output.append("\t\t").append(line).append("\n")));
output.append("}\n");
return output.toString();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt( mIsTaskNode ? 1 : 0);
if (mIsTaskNode) {
dest.writeInt(mTaskId);
} else {
mName.writeToParcel(dest, 0);
}
if (mChildren.size() == 0 || mChildren == null) {
dest.writeInt(0);
} else {
dest.writeInt(1);
dest.writeTypedList(mChildren);
}
dest.writeInt(mOptionFlags);
dest.writeInt(mIntent == null ? 0 : 1);
if (mIntent != null) mIntent.writeToParcel(dest, 0 /* flags */);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<Node> CREATOR = new Creator<Node>() {
@Override
public Node createFromParcel(Parcel in) {
return new Node(in);
}
@Override
public Node[] newArray(int size) {
return new Node[size];
}
};
}

View File

@@ -0,0 +1,22 @@
/*
* 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.example.android.intentplayground;
/**
* A plain activity with no activity flags set and default taskAffinity.
*/
public class RegularActivity extends BaseActivity {
}

View File

@@ -1,76 +1,28 @@
/** TODO: http://go/java-style#javadoc */
/*
* Copyright (C) 2017 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.example.android.intentplayground;
import android.app.Fragment;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.View;
import java.util.List;
import java.util.Map;
class TestBase {
static final String EXPECTED_HIERARCHY = "";
enum LaunchStyle { TASK_STACK_BUILDER, COMMAND_LINE, LAUNCH_FORWARD }
TestBase(Context context, Node hierarchy) {}
void setupActivities(LaunchStyle style) {}
Node computeExpected(Intent intent) { return null; }
static Node describeTaskHierarchy(Context context) { return null; }
}
class TreeFragment extends Fragment {
static final String TREE_NODE = "";
static final String FRAGMENT_TITLE = "";
}
class CurrentTaskFragment extends Fragment {}
class IntentFragment extends Fragment {}
class IntentBuilderFragment extends Fragment implements View.OnClickListener {
ComponentName mActivityToLaunch;
void selectLaunchMode(View view) {}
public void onClick(View view) {}
void clearFlags() {}
void selectFlags(List<String> flags) {}
}
class BuildConfig {
static final boolean DEBUG = true;
}
class Node implements Parcelable, Comparable<Node> {
ComponentName name;
boolean isTaskNode;
int taskId;
static final String TAG = "";
public static final Creator<Node> CREATOR = new Creator<Node>() {
@Override
public Node createFromParcel(Parcel in) {
return new Node(in);
}
@Override
public Node[] newArray(int size) {
return new Node[size];
}
};;
List<Node> children;
Node(ComponentName data) {}
Node(int taskId) {}
Node(Node other) {}
Node(Parcel in) {}
Node addChild(Node child) { return null; }
boolean equals(Node other) { return false; }
public int compareTo(Node o) {return 0;}
@Override
public void writeToParcel(Parcel dest, int flags) {}
@Override
public int describeContents() { return 0; }
class ResultFragment extends Fragment implements View.OnClickListener {
public void onClick(View view) {}
}
class FlagUtils {
static List<String> discoverFlags(Intent intent) { return null; }
static List<String> intentFlags() { return null; }
static Map<String, List<String>> intentFlagsByCategory() { return null; }
static int value(String flagName) { return 0; }
static List<String> discoverActivityFlags() { return null; }
static String camelify(String snake) { return null; }
}

View File

@@ -0,0 +1,230 @@
/*
* 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.example.android.intentplayground;
import android.app.Fragment;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v4.view.PagerTitleStrip;
import android.support.v4.view.ViewPager;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ScrollView;
import java.util.LinkedList;
import java.util.List;
/**
* Displays a help overlay over the current activity.
*/
public class ShowcaseFragment extends Fragment {
private ViewGroup mRoot;
private List<Step> mSteps = new LinkedList<>();
private ViewPager mPager;
private StepAdapter mAdapter;
private ScrollView mScrollView;
private View mOldTarget;
private Drawable mOldTargetBackground;
private DisplayMetrics mDisplayMetrics = new DisplayMetrics();
private Runnable mUserOnFinish;
private int mIndex = 0;
private static final int SCROLL_OFFSET = 50;
private static final float HIGHLIGHT_ELEVATION = 4;
@Override
public void onAttach(Context context) {
super.onAttach(context);
mAdapter = new StepAdapter(context, mSteps);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
Context context = getContext();
mRoot = container;
FrameLayout backgroundLayout = new FrameLayout(context);
mPager = new ViewPager(context);
PagerTitleStrip pagerTitleView = new PagerTitleStrip(context);
pagerTitleView.setGravity(Gravity.TOP);
ViewPager.LayoutParams params = new ViewPager.LayoutParams();
params.width = ViewPager.LayoutParams.MATCH_PARENT;
params.height = ViewPager.LayoutParams.MATCH_PARENT;
mPager.setLayoutParams(params);
backgroundLayout.setLayoutParams(params);
params.height = ViewPager.LayoutParams.WRAP_CONTENT;
params.isDecor = true;
pagerTitleView.setLayoutParams(params);
mPager.addView(pagerTitleView);
backgroundLayout.addView(mPager);
mAdapter.setButtonCallbacks(
/* onFinish */ view -> {
cancel();
mScrollView.scrollTo(0, 0);
},
/* onCancel */ view -> cancel(),
/* onNext */ view -> next()
);
mPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int i, float v, int i1) {}
@Override
public void onPageScrollStateChanged(int i) {}
@Override
public void onPageSelected(int i) {
executeStep(i);
}
});
mPager.setAdapter(mAdapter);
return backgroundLayout;
}
@Override
public void onStart() {
super.onStart();
// Get display metrics for converting dp to px
getActivity().getWindowManager().getDefaultDisplay().getMetrics(mDisplayMetrics);
// Scroll to and highlight first step
executeStep(mIndex);
}
@Override
public void onStop() {
super.onStop();
clearHighlight();
if (mUserOnFinish != null) mUserOnFinish.run();
}
public void setScroller(ScrollView scroller) {
mScrollView = scroller;
}
public void addStep(Step step) {
mSteps.add(step);
if (mAdapter != null) mAdapter.notifyDataSetChanged();
}
public void addStep(@StringRes int tutorialText, @IdRes int targetView) {
addStep(new Step(tutorialText, targetView));
}
public void addStep(@StringRes int tutorialText, @IdRes int targetView,
@IdRes int highlightTargetView) {
addStep(new Step(tutorialText, targetView, highlightTargetView));
}
public void addStep(@StringRes int tutorialText,
@IdRes int targetView,
Runnable callback) {
addStep(new Step(tutorialText, targetView, callback));
}
/**
* Advances the pager to the next step.
*/
public void next() {
mPager.setCurrentItem(++mIndex);
}
/**
* Shows the indicated page.
* @param i The index of the page to show.
*/
private void executeStep(int i) {
Step current = mAdapter.getStep(i);
View target = mRoot.findViewById(current.targetViewRes);
View highlightTarget = current.highlightTargetViewRes != 0 ?
mRoot.findViewById(current.highlightTargetViewRes) : target;
target.getParent().requestChildFocus(target, target);
mScrollView.smoothScrollTo(0, Float.valueOf(target.getTop()).intValue()
- SCROLL_OFFSET);
highlightView(highlightTarget);
if (current.callback != null) current.callback.run();
}
/**
* Destroys this fragment.
*/
public void cancel() {
getActivity().getFragmentManager().beginTransaction().remove(this).commit();
}
private void clearHighlight() {
if (mOldTarget != null) {
mOldTarget.setBackground(mOldTargetBackground);
mOldTarget = null;
}
mRoot.setBackground(null); // Clear root background
}
private void highlightView(View target) {
Resources res = getContext().getResources();
clearHighlight();
mOldTarget = target;
mOldTargetBackground = target.getBackground();
target.setBackground(res.getDrawable(R.drawable.showcase_background, null /* theme*/));
target.setElevation(HIGHLIGHT_ELEVATION * mDisplayMetrics.density);
// Dull parent background
mRoot.setBackground(res.getDrawable(R.drawable.shade, null /* theme */));
}
/**
* Set a callback to be run in the onStop() method
* @param onFinish Callback to be run when the Showcase is finished
*/
public void setOnFinish(Runnable onFinish) {
this.mUserOnFinish = onFinish;
}
/**
* Represents a page in {@link ViewPager}, with associated text to show and a target element
* to scroll to.
*/
public class Step {
@StringRes public int tutorialText;
@IdRes public int targetViewRes;
@IdRes public int highlightTargetViewRes;
public Runnable callback;
public Step(@StringRes int tutorialSentence, @IdRes int targetView) {
tutorialText = tutorialSentence;
targetViewRes = targetView;
}
public Step(@StringRes int tutorialSentence, @IdRes int targetView,
@IdRes int highlightTargetView) {
tutorialText = tutorialSentence;
targetViewRes = targetView;
highlightTargetViewRes = highlightTargetView;
}
public Step(@StringRes int tutorialSentence, @IdRes int targetView,
Runnable onStepCallback) {
tutorialText = tutorialSentence;
targetViewRes = targetView;
callback = onStepCallback;
}
}
}

View File

@@ -17,6 +17,6 @@
package com.example.android.intentplayground;
/**
* An activity with launchMode="singleTask" set in AndroidManifest.xml
* An activity with launchMode="singleTask" set in AndroidManifest.xml.
*/
public class SingleTaskActivity extends BaseActivity {}

View File

@@ -17,6 +17,6 @@
package com.example.android.intentplayground;
/**
* An activity with launchMode="singleTop" set in AndroidManifest.xml
* An activity with launchMode="singleTop" set in AndroidManifest.xml.
*/
public class SingleTopActivity extends BaseActivity {}

View File

@@ -0,0 +1,105 @@
/*
* 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.example.android.intentplayground;
import android.content.Context;
import android.content.res.Resources;
import android.support.v4.view.PagerAdapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import java.util.List;
/**
* A {@link PagerAdapter} for {@link ShowcaseFragment} that handles the creation of Views based
* on a list of {@link com.example.android.intentplayground.ShowcaseFragment.Step}.
*/
class StepAdapter extends PagerAdapter {
private final Context mContext;
private View.OnClickListener mNextCallback;
private View.OnClickListener mCancelCallback;
private View.OnClickListener mFinishCallback;
private List<ShowcaseFragment.Step> mSteps;
private LayoutInflater mInflater;
/**
* Constructs a new StepAdapter.
* @param context The context that holds this adapter.
* @param steps A list of {@link com.example.android.intentplayground.ShowcaseFragment.Step}s
*/
public StepAdapter(Context context, List<ShowcaseFragment.Step> steps) {
mContext = context;
mSteps = steps;
mInflater = LayoutInflater.from(mContext);
}
/**
* Set the callbacks to be run when pager buttons are clicked.
* @param finish The method to run when the finish action is requested.
* @param cancel The method to run when the cancel action is requested.
* @param next The method to run when the next action is requested.
*/
public void setButtonCallbacks(View.OnClickListener finish, View.OnClickListener cancel,
View.OnClickListener next) {
mFinishCallback = finish;
mCancelCallback = cancel;
mNextCallback = next;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
ShowcaseFragment.Step currentStep = getStep(position);
ViewGroup skeletonLayout = (ViewGroup) mInflater
.inflate(R.layout.showcase_skeleton, container, false /* attachToRoot */);
TextView tutorialText = skeletonLayout.findViewById(R.id.tutorial_text);
tutorialText.setText(mContext.getString(currentStep.tutorialText));
Button cancelButton = skeletonLayout.findViewById(R.id.cancel_pager);
Button nextButton = skeletonLayout.findViewById(R.id.next_pager);
if (position == getCount() - 1) {
// last item, adjust button bar
cancelButton.setVisibility(View.GONE);
nextButton.setText(R.string.help_step_finish);
nextButton.setOnClickListener(mFinishCallback);
} else {
cancelButton.setOnClickListener(mCancelCallback);
nextButton.setOnClickListener(mNextCallback);
}
container.addView(skeletonLayout);
return skeletonLayout;
}
@Override
public void destroyItem(ViewGroup container, int position, Object view) {
container.removeView((View) view);
}
public ShowcaseFragment.Step getStep(int i) {
return mSteps.get(i);
}
@Override
public int getCount() {
return mSteps.size();
}
@Override
public boolean isViewFromObject(View view, Object o) {
return view.equals(o);
}
}

View File

@@ -17,6 +17,6 @@
package com.example.android.intentplayground;
/**
* An activity with taskAffinity=".t1" set in AndroidManifest.xml
* An activity with taskAffinity=".t1" set in AndroidManifest.xml.
*/
public class TaskAffinity1Activity extends BaseActivity {}

View File

@@ -17,6 +17,6 @@
package com.example.android.intentplayground;
/**
* An activity with taskAffinity=".t2" set in AndroidManifest.xml
* An activity with taskAffinity=".t2" set in AndroidManifest.xml.
*/
public class TaskAffinity2Activity extends BaseActivity {}

View File

@@ -17,6 +17,6 @@
package com.example.android.intentplayground;
/**
* An activity with taskAffinity=".t3" set in AndroidManifest.xml
* An activity with taskAffinity=".t3" set in AndroidManifest.xml.
*/
public class TaskAffinity3Activity extends BaseActivity {}

View File

@@ -0,0 +1,98 @@
/*
* 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.example.android.intentplayground;
import android.app.ActivityManager.RecentTaskInfo;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.util.Log;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public final class TaskInfo {
private final static String TAG = "TaskInfo";
/**
* Access the ActivityInstanceInfo without depending on it at compile time.
*
* @param info the recent task mInfo to try and access the activity information of.
* @return The activity instance mInfo in our own mirrored value object or an empty list.
*/
public static List<ActivityInstanceInfoMirror> getActivities(RecentTaskInfo info) {
try {
List<Object> activities = (List<Object>) info.getClass().getField("activities").get(info);
List<ActivityInstanceInfoMirror> activityInfoMirrors = new ArrayList<>();
for (Object activityInstanceInfo : activities) {
Class<?> activityInfoClass = activityInstanceInfo.getClass();
System.out.println(Arrays.toString(activityInfoClass.getDeclaredFields()));
ActivityInfo activityInfo = (ActivityInfo) activityInfoClass.getMethod("getInfo")
.invoke(activityInstanceInfo);
Intent intent = (Intent) activityInfoClass.getMethod("getIntent")
.invoke(activityInstanceInfo);
ComponentName name = (ComponentName) activityInfoClass.getMethod("getName")
.invoke(activityInstanceInfo);
Integer hashId = (Integer) activityInfoClass.getMethod("getHashId")
.invoke(activityInstanceInfo);
activityInfoMirrors.add(new ActivityInstanceInfoMirror(activityInfo, intent, name, hashId));
}
return activityInfoMirrors;
} catch (IllegalAccessException | NoSuchFieldException | InvocationTargetException | NoSuchMethodException e) {
Log.e(TAG, "ActivityInstanceInfo not available on the current api level", e);
return new ArrayList<>();
}
}
//TODO (b/119894108): Replace the mirror with actual class once the framework-api is merged.
public static class ActivityInstanceInfoMirror {
private final ActivityInfo mInfo;
private final Intent mIntent;
private final ComponentName mComponentName;
private final int mHashId;
public ActivityInstanceInfoMirror(ActivityInfo info, Intent intent,
ComponentName componentName, int hashId) {
this.mInfo = info;
this.mIntent = intent;
this.mComponentName = componentName;
this.mHashId = hashId;
}
public ActivityInfo getInfo() {
return mInfo;
}
public Intent getIntent() {
return mIntent;
}
public ComponentName getName() {
return mComponentName;
}
public int getHashId() {
return mHashId;
}
}
}

View File

@@ -0,0 +1,369 @@
/*
* 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.example.android.intentplayground;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.TaskStackBuilder;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.support.annotation.Nullable;
import android.util.Log;
import com.example.android.intentplayground.TaskInfo.ActivityInstanceInfoMirror;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static com.example.android.intentplayground.FlagUtils.getActivityFlags;
import static com.example.android.intentplayground.FlagUtils.hasActivityFlag;
import static com.example.android.intentplayground.FlagUtils.hasIntentFlag;
import static java.util.Collections.singletonList;
/**
* TestBase holds methods to query, test and compare task hierarchies.
*/
public class TestBase {
static final String TAG = "TestBase";
private List<TaskStackBuilder> mBuilders;
private Context mContext;
private PackageInfo mPackageInfo;
TestBase(Context context, Node hierarchy) {
mBuilders = new LinkedList<>();
mContext = context;
setActivities(hierarchy);
}
/**
* Launch the activities specified by the constructor.
*
* @param style An enum that chooses which method to use to launch the activities.
*/
void startActivities(LaunchStyle style) {
switch (style) {
// COMMAND_LINE will only work if the application is installed with system permissions
// that allow it to use am shell command "am start ..."
case COMMAND_LINE:
mBuilders.forEach(tsb -> Arrays.stream(tsb.getIntents())
.forEach(AMControl::launchInBackground));
break;
case TASK_STACK_BUILDER:
mBuilders.forEach(tsb -> {
// TODO: does this indicate bug in ActivityManager?
// The launch of each activity needs to be delayed a bit or ActivityManager will7
// skip creating most of them
try {
Thread.sleep(500);
tsb.startActivities();
Thread.sleep(500);
} catch (InterruptedException ie) {
Log.e(LauncherActivity.TAG, ie.getMessage());
}
});
break;
case LAUNCH_FORWARD:
mBuilders.forEach(tsb -> {
// The launch of each activity needs to be delayed a bit or ActivityManager will
// skip creating most of them
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
Log.e(LauncherActivity.TAG, ie.getMessage());
}
ArrayList<Intent> nextIntents = new ArrayList<>(Arrays.asList(
tsb.getIntents()));
Intent launch = nextIntents.remove(0)
.putParcelableArrayListExtra(BaseActivity.EXTRA_LAUNCH_FORWARD, nextIntents);
if (BuildConfig.DEBUG) {
Log.d(TAG, "Launching " + launch.getComponent().toString());
}
mContext.startActivity(launch);
});
break;
}
}
/**
* This method examines the flags on the given intent, as well as the <activity> flags of
* the intended component, and computes what the hierarchy of activities should look like
* after the launch of the intended component (WORK IN PROGRESS).
* @param intent The intent that will be passed to startActivity().
* @return A Node object that models the expected hierarchy.
*/
Node computeExpected(Intent intent) {
// Determine the effect of selected mIntent flags on expected hierarchy
Node currentTasks = describeTaskHierarchy(mContext);
Node startActivity = new Node(intent.getComponent()).setIntent(intent);
Node targetTask = findReusableTarget(currentTasks, intent)
.orElseGet(() -> createNewTaskIfNeeded(currentTasks, intent)
.orElse(findCurrentTask(currentTasks).get()));
clearIfNeeded(targetTask, intent);
if (needsStartActivity(currentTasks, intent)) {
targetTask.addFirstChild(startActivity);
}
return currentTasks;
}
/**
* Finds the taskAffinity of the target component
* @param actName The component for which to find the corresponding affinity
* @return the task affinity, or null if there is none associated with the component
*/
Optional<String> affinityOf(ComponentName actName) {
String affinity = null;
for (ActivityInfo activityInfo : mPackageInfo.activities) {
if (activityInfo.name.equals(actName.getClassName())) {
affinity = activityInfo.taskAffinity;
}
}
return Optional.ofNullable(affinity);
}
/**
* Describes the current set of tasks open in the application as
* a tree of Nodes. Returns a root node, whose children are task nodes.
* The children of those task nodes are activities, in order of most recently used.
* @param context The context of an activity in the application.
* @return A Node that models the current task hierarchy of the application.
*/
public static Node describeTaskHierarchy(Context context) {
ActivityManager am = context.getSystemService(ActivityManager.class);
Node root = Node.newRootNode();
int currentTaskId = ((Activity) context).getTaskId();
List<ActivityManager.RecentTaskInfo> tasks = am.getAppTasks().stream()
.map(ActivityManager.AppTask::getTaskInfo).collect(Collectors.toList());
for (ActivityManager.AppTask task : am.getAppTasks()) {
ActivityManager.RecentTaskInfo rti = task.getTaskInfo();
List<ActivityInstanceInfoMirror> activities = TaskInfo.getActivities(rti);
if (!activities.isEmpty()) {
Intent baseIntent = activities.get(0).getIntent();
Node taskRoot = new Node(rti.persistentId).setIntent(baseIntent);
if (taskRoot.mTaskId == currentTaskId) taskRoot.setCurrent(true);
activities.forEach(activity -> {
taskRoot.addChild(new Node(activity.getName().clone())
.setIntent(activity.getIntent()));
});
root.addChild(taskRoot);
}
}
return root;
}
private Optional<Node> findReusableTarget(Node tasks, Intent intent) {
ComponentName target = intent.getComponent();
boolean hasNewTask = hasIntentFlag(intent, IntentFlag.NEW_TASK);
boolean hasMultipleTask = hasIntentFlag(intent, IntentFlag.MULTIPLE_TASK);
boolean isSingleInstance = hasActivityFlag(mContext, target,
ActivityFlag.LAUNCH_MODE_SINGLE_INSTANCE);
boolean isSingleTask = hasActivityFlag(mContext, target,
ActivityFlag.LAUNCH_MODE_SINGLE_TASK);
boolean isIntoExisting = hasActivityFlag(mContext, target,
ActivityFlag.DOCUMENT_LAUNCH_MODE_INTO_EXISTING);
if (isSingleInstance || isSingleTask) {
Log.d(TAG, "found resuable target singleInstance/singleTask");
return findTaskOfActivity(tasks, target);
} else if (isIntoExisting || (hasNewTask && !hasMultipleTask)) {
Optional<Node> rootTask = findTaskWithRoot(tasks, target);
if (rootTask.isPresent()) {
Log.d(TAG, "found resuable target, same root task");
return rootTask;
}
else if (!isDocument(intent)) {
Log.d(TAG, "found resuable target, same affinity task");
return findTaskWithAffinity(tasks, affinityOf(target).
orElse(target.getPackageName()));
}
}
Log.d(TAG, "did not find resuable target");
return Optional.empty();
}
private Optional<Node> createNewTaskIfNeeded(Node tasks, Intent intent) {
// Everything in this method runs assuming there is no reuseable target for the intent
ComponentName target = intent.getComponent();
boolean hasNewTask = hasIntentFlag(intent, IntentFlag.NEW_TASK);
boolean hasMultipleTask = hasIntentFlag(intent, IntentFlag.MULTIPLE_TASK);
boolean hasNewDocument = hasIntentFlag(intent, IntentFlag.NEW_DOCUMENT);
Set<ActivityFlag> flags = getActivityFlags(mContext, target);
boolean isSingleInstance = flags.contains(ActivityFlag.LAUNCH_MODE_SINGLE_INSTANCE);
boolean isSingleTask = flags.contains(ActivityFlag.LAUNCH_MODE_SINGLE_TASK);
boolean isDocument = flags.contains(ActivityFlag.DOCUMENT_LAUNCH_MODE_ALWAYS)
|| flags.contains(ActivityFlag.DOCUMENT_LAUNCH_MODE_INTO_EXISTING);
if (hasNewTask || hasNewDocument || isDocument || isSingleInstance || isSingleTask) {
if (hasMultipleTask) {
// remove task with same root if present
findTaskWithRoot(tasks, target).ifPresent(task -> {
tasks.mChildren.remove(task);
});
}
Node newNode = new Node(Node.NEW_TASK_ID).setIntent(intent);
newNode.setNew(true);
tasks.addFirstChild(newNode);
Log.d(TAG, "create new task");
return Optional.of(newNode);
}
Log.d(TAG, "did not create new task");
return Optional.empty();
}
private static Optional<Node> findCurrentTask(Node stack) {
return stack.mChildren.stream().filter(Node::isCurrent).findFirst();
}
private static Optional<Node> findTaskOfActivity(Node stack, ComponentName target) {
for (Node task : stack.mChildren) {
for (Node activity : task.mChildren) {
if (activity.mName.equals(target)) return Optional.of(task);
}
}
return Optional.empty();
}
public static Optional<Node> findTaskWithRoot(Node stack, ComponentName target) {
return stack.mChildren.stream().filter(task -> task.mChildren.get(task.mChildren.size() - 1)
.mName.equals(target))
.findFirst();
}
public static List<Node> findTasksWithRoot(Node stack, ComponentName target) {
return stack.mChildren.stream().filter(task -> task.mChildren.get(task.mChildren.size() - 1)
.mName.equals(target))
.collect(Collectors.toList());
}
private Optional<Node> findTaskWithAffinity(Node stack, String affinity) {
// Need to iterate through tasks from least to most recent
ListIterator<Node> iterator = stack.mChildren.listIterator(stack.mChildren.size());
while (iterator.hasPrevious()) {
Node task = iterator.previous();
if (!hasActivityFlag(mContext, task.mChildren.get(0).mName,
ActivityFlag.LAUNCH_MODE_SINGLE_INSTANCE)) { // Exclude singleInstance
if (affinityOf(task.mChildren.get(0).mName)
.filter(a -> a.equals(affinity)).isPresent()) { // find matching affinity
return Optional.of(task);
}
}
}
return Optional.empty();
}
private boolean needsStartActivity(Node tasks, Intent intent) {
ComponentName sourceActivity = findCurrentTask(tasks).orElse(tasks.mChildren.get(0))
.mChildren.get(0).mName;
ComponentName target = intent.getComponent();
boolean hasSingleTop = hasIntentFlag(intent, IntentFlag.SINGLE_TOP);
boolean isSingleInstance = hasActivityFlag(mContext, target,
ActivityFlag.LAUNCH_MODE_SINGLE_INSTANCE);
boolean isSingleTask = hasActivityFlag(mContext, target,
ActivityFlag.LAUNCH_MODE_SINGLE_TASK);
boolean isSingleTop = hasActivityFlag(mContext, target,
ActivityFlag.LAUNCH_MODE_SINGLE_TOP);
if (sourceActivity.equals(target) &&
(isSingleInstance || isSingleTask || isSingleTop || hasSingleTop )) {
return false;
}
return true;
}
private boolean isDocument(Intent intent) {
Set<ActivityFlag> flags = getActivityFlags(mContext, intent.getComponent());
return flags.contains(ActivityFlag.DOCUMENT_LAUNCH_MODE_ALWAYS) ||
flags.contains(ActivityFlag.DOCUMENT_LAUNCH_MODE_INTO_EXISTING) ||
hasIntentFlag(intent, IntentFlag.NEW_DOCUMENT);
}
void setActivities(Node hierarchy) {
// load the package info for this app (used later to get <activity> flags)
PackageManager pm = mContext.getPackageManager();
try {
mPackageInfo = pm.getPackageInfo(mContext.getPackageName(),
PackageManager.GET_ACTIVITIES);
} catch (PackageManager.NameNotFoundException e) {
// Should not happen
throw new RuntimeException(e);
}
// Build list of TaskStackBuilders from task hierarchy modeled by Node
if (hierarchy.mChildren.isEmpty()) return;
mBuilders.clear();
hierarchy.mChildren.forEach(taskParent -> {
TaskStackBuilder tb = TaskStackBuilder.create(mContext);
Intent taskRoot = new Intent()
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
.setComponent(taskParent.mChildren.get(0).mName);
tb.addNextIntent(taskRoot);
taskParent.mChildren.subList(1, taskParent.mChildren.size()).forEach(activity ->
tb.addNextIntent(new Intent().setComponent(activity.mName)));
mBuilders.add(tb);
});
// Edit the mIntent of the last activity in the last task so that it will relaunch the
// activity that constructed this TestBase
TaskStackBuilder tsb = mBuilders.get(mBuilders.size() - 1);
Intent lastIntent = tsb.editIntentAt(tsb.getIntentCount() - 1);
Intent launcherIntent = new Intent(mContext, mContext.getClass());
lastIntent.putParcelableArrayListExtra(BaseActivity.EXTRA_LAUNCH_FORWARD,
new ArrayList<>(singletonList(launcherIntent)));
}
private void clearIfNeeded(Node task, Intent intent) {
ComponentName target = intent.getComponent();
boolean hasClearTop = hasIntentFlag(intent, IntentFlag.CLEAR_TOP);
boolean shouldClearTask = hasIntentFlag(intent, IntentFlag.CLEAR_TASK)
&& hasIntentFlag(intent, IntentFlag.NEW_TASK);
boolean isDocument = isDocument(intent);
boolean isSingleInstance = hasActivityFlag(mContext, target,
ActivityFlag.LAUNCH_MODE_SINGLE_INSTANCE);
boolean isSingleTask = hasActivityFlag(mContext, target,
ActivityFlag.LAUNCH_MODE_SINGLE_TASK);
if (hasClearTop) {
int targetIndex = 0;
for (int i = 0; i < task.mChildren.size(); i++) {
if (task.mChildren.get(i).mName.equals(target)) targetIndex = i;
}
task.mChildren = task.mChildren.subList(targetIndex, task.mChildren.size());
} else if (shouldClearTask || isDocument || isSingleInstance || isSingleTask) {
task.clearChildren();
}
}
public static void clearRunningTasks(Context context) {
ComponentName launcher = new ComponentName(context, LauncherActivity.class);
context.getSystemService(ActivityManager.class).getAppTasks().stream()
.filter(task -> {
ActivityManager.RecentTaskInfo info = task.getTaskInfo();
return (info.baseActivity != null) && (!info.baseActivity.equals(launcher));
})
.forEach(ActivityManager.AppTask::finishAndRemoveTask);
}
public Context getContext() { return mContext; }
/**
* An enum representing options for launching a series of tasks using this TestBase.
*/
enum LaunchStyle { TASK_STACK_BUILDER, COMMAND_LINE, LAUNCH_FORWARD}
}

View File

@@ -0,0 +1,158 @@
/*
* 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.example.android.intentplayground;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
/**
* This fragment displays a hierarchy of tasks and activities in an expandable list.
*/
public class TreeFragment extends Fragment {
public static final String TREE_NODE = "com.example.android.NODE_TREE";
public static final String FRAGMENT_TITLE = "com.example.android.TREE_FRAGMENT_TITLE";
private Activity mActivity;
private Node mTree;
private String mTitle;
private ViewGroup mContainer;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Bundle args = getArguments();
if (args != null) {
mTree = args.getParcelable(TREE_NODE);
mTitle = args.getString(FRAGMENT_TITLE);
}
return inflater.inflate(R.layout.fragment_tree, container, false /* attachToRoot */);
}
@Override
public void onResume() {
super.onResume();
mActivity = getActivity();
LinearLayout treeLayout = (LinearLayout) getView();
LinearLayout treeView = treeLayout.findViewById(R.id.task_tree);
mContainer = treeView;
TextView titleView = treeLayout.findViewById(R.id.task_tree_title);
if (mTitle != null) {
titleView.setText(mTitle);
}
if (mTree != null) {
displayHierarchy(mTree, treeView);
} else {
displayHierarchy(TestBase.describeTaskHierarchy(mActivity), treeView);
}
}
/**
* Takes a Node and creates views corresponding to the task hierarchy
* @param tree a {@link Node} that models the task hierarchy
* @param container the {@link LinearLayout} in which to display them
*/
protected void displayHierarchy(Node tree, LinearLayout container) {
ExpandableAdapter adapter = new ExpandableAdapter(getActivity(), tree);
View view;
// fill container
container.removeAllViews();
for (int i = 0; i < adapter.getGroupCount(); i++) {
view = makeCompositeView(adapter, container, i);
if (view != null) container.addView(view);
}
}
private View makeCompositeView(ExpandableAdapter adapter, ViewGroup parent, int group) {
LayoutInflater inflater = getLayoutInflater();
LinearLayout compositeLayout = (LinearLayout) inflater
.inflate(R.layout.tree_node_composite, parent, false /* attachToRoot */);
LinearLayout parentLayout = compositeLayout.findViewById(R.id.group_item);
LinearLayout childLayout = compositeLayout.findViewById(R.id.child_item);
LinearLayout buttonBarlayout = compositeLayout.findViewById(R.id.move_task_to_front_bar);
if (adapter.getChildrenCount(group) == 0) {
return null;
}
parentLayout.addView(adapter.getGroupView(group, false /* isExpanded */,
null /* convertView */, parent));
for (int i = 0; i < adapter.getChildrenCount(group); i++) {
childLayout.addView(adapter.getChildView(group, i, false /* isLastChild */,
null /* convertView */, parentLayout));
}
compositeLayout.setOnClickListener(view -> {
LinearLayout childView1 = view.findViewById(R.id.child_item);
childView1.setVisibility(childView1.getVisibility() == View.GONE ?
View.VISIBLE : View.GONE);
if (group > 0) {
buttonBarlayout.setVisibility(buttonBarlayout.getVisibility() == View.GONE ?
View.VISIBLE : View.GONE);
}
parentLayout.removeAllViews();
parentLayout.addView(adapter.getGroupView(group,
!(childView1.getVisibility() == View.GONE), null /* convertView */, parent));
});
// Set a no-op childView click listener so the event doesn't bubble up to the composite view
childLayout.setOnClickListener(view -> {});
//Set onclick listener for button bar
int taskId = ((Node) adapter.getGroup(group)).mTaskId;
if (group == 0) {
// hide the button bar, it is the current task
buttonBarlayout.setVisibility(View.GONE);
} else {
int color = mActivity.getResources()
.getColor(ColorManager.getColorForTask(taskId), null /* theme */);
Button moveTaskButton = buttonBarlayout.findViewById(R.id.move_task_to_front_button);
moveTaskButton.setOnClickListener(view -> moveTaskToFront(taskId));
moveTaskButton.setTextColor(color);
Button removeTaskButton = buttonBarlayout.findViewById(R.id.kill_task_button);
removeTaskButton.setOnClickListener(view -> removeTask(taskId));
removeTaskButton.setTextColor(color);
}
return compositeLayout;
}
private void removeTask(int taskId) {
ActivityManager am = mActivity.getSystemService(ActivityManager.class);
am.getAppTasks().forEach(task -> {
if (task.getTaskInfo().persistentId == taskId) {
task.finishAndRemoveTask();
}
});
onResume(); // manually trigger UI refresh
}
private void moveTaskToFront(int taskId) {
ActivityManager am = mActivity.getSystemService(ActivityManager.class);
am.moveTaskToFront(taskId, 0);
}
/**
* Expand a task group to show its child activities.
* @param i The index of the task to expand.
*/
public void openTask(int i) {
View taskView = mContainer.getChildAt(i);
if (taskView != null) taskView.callOnClick();
}
}