1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.android.intentplayground; 18 19 import static com.example.android.intentplayground.Node.newTaskNode; 20 21 import android.app.Activity; 22 import android.content.ComponentName; 23 import android.content.Intent; 24 import android.os.Bundle; 25 import com.google.android.material.floatingactionbutton.FloatingActionButton; 26 import android.util.Log; 27 import android.view.Menu; 28 import android.view.MenuItem; 29 30 import androidx.appcompat.app.AlertDialog; 31 import androidx.appcompat.app.AppCompatActivity; 32 import androidx.appcompat.widget.Toolbar; 33 import androidx.fragment.app.FragmentManager; 34 import androidx.fragment.app.FragmentTransaction; 35 import androidx.lifecycle.ViewModelProvider; 36 37 import com.example.android.intentplayground.Tracking.Tracker; 38 39 import java.util.ArrayList; 40 import java.util.List; 41 import java.util.function.Consumer; 42 43 /** 44 * Implements the shared functionality for all of the other activities. 45 */ 46 public abstract class BaseActivity extends AppCompatActivity implements 47 IntentBuilderView.OnLaunchCallback { 48 public final static String EXTRA_LAUNCH_FORWARD = "com.example.android.launchForward"; 49 public final static String BUILDER_VIEW = "com.example.android.builderFragment"; 50 public static final String TREE_FRAGMENT = "com.example.android.treeFragment"; 51 public static final String EXPECTED_TREE_FRAGMENT = "com.example.android.expectedTreeFragment"; 52 public static final int LAUNCH_REQUEST_CODE = 0xEF; 53 private static final int LAUNCH_FOR_RESULT_ID = 1; 54 55 public enum Mode {LAUNCH, VERIFY, RESULT} 56 57 public boolean userLeaveHintWasCalled = false; 58 protected Mode mStatus = Mode.LAUNCH; 59 60 /** 61 * To display the task / activity overview in {@link TreeFragment} we track onResume and 62 * onDestroy calls in this global location. {@link BaseActivity} should delegate to 63 * {@link Tracker#onResume(Activity)} and {@link Tracker#onDestroy(Activity)} in it's respective 64 * lifecycle callbacks. 65 */ 66 private static Tracker mTracker = new Tracker(); 67 68 @Override onCreate(Bundle savedInstanceState)69 protected void onCreate(Bundle savedInstanceState) { 70 super.onCreate(savedInstanceState); 71 setContentView(R.layout.activity_main); 72 if (BuildConfig.DEBUG) Log.d(getLocalClassName(), "onCreate()"); 73 // Setup action bar 74 Toolbar appBar = findViewById(R.id.app_bar); 75 appBar.setTitle(this.getClass().getSimpleName()); 76 setSupportActionBar(appBar); 77 78 FloatingActionButton launchButton = findViewById(R.id.launch_fab); 79 launchButton.setOnClickListener(l -> { 80 LaunchFragment fragment = new LaunchFragment(); 81 82 getSupportFragmentManager().beginTransaction() 83 .addToBackStack(null) 84 .replace(R.id.fragment_container, fragment) 85 .commit(); 86 }); 87 88 BaseActivityViewModel viewModel = (new ViewModelProvider(this, 89 new ViewModelProvider.NewInstanceFactory())).get(BaseActivityViewModel.class); 90 91 viewModel.getFabActions().observe(this, action -> { 92 switch (action) { 93 case Show: 94 launchButton.show(); 95 break; 96 case Hide: 97 launchButton.hide(); 98 break; 99 } 100 }); 101 102 103 loadMode(Mode.LAUNCH); 104 } 105 106 @Override onResume()107 protected void onResume() { 108 super.onResume(); 109 mTracker.onResume(this); 110 Intent launchForward = prepareLaunchForward(); 111 if (launchForward != null) { 112 startActivity(launchForward); 113 } 114 } 115 116 @Override onDestroy()117 protected void onDestroy() { 118 super.onDestroy(); 119 mTracker.onDestroy(this); 120 } 121 addTrackerListener(Consumer<List<Tracking.Task>> listener)122 static void addTrackerListener(Consumer<List<Tracking.Task>> listener) { 123 mTracker.addListener(listener); 124 } 125 removeTrackerListener(Consumer<List<Tracking.Task>> listener)126 static void removeTrackerListener(Consumer<List<Tracking.Task>> listener) { 127 mTracker.removeListener(listener); 128 } 129 130 /** 131 * Initializes the UI for the specified {@link Mode}. 132 * 133 * @param mode The mode to display. 134 */ loadMode(Mode mode)135 protected void loadMode(Mode mode) { 136 FragmentManager fragmentManager = getSupportFragmentManager(); 137 138 if (fragmentManager.findFragmentById(R.id.fragment_container) == null) { 139 FragmentTransaction transaction = fragmentManager.beginTransaction() 140 .setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out); 141 if (mode == Mode.LAUNCH) { 142 TreeFragment currentTaskFragment = new TreeFragment(); 143 Bundle args = new Bundle(); 144 args.putString(TreeFragment.FRAGMENT_TITLE, 145 getString(R.string.current_task_hierarchy_title)); 146 currentTaskFragment.setArguments(args); 147 transaction.add(R.id.fragment_container, currentTaskFragment, TREE_FRAGMENT); 148 transaction.add(R.id.fragment_container, new IntentFragment()); 149 transaction.commit(); 150 151 mStatus = Mode.LAUNCH; 152 } 153 } 154 } 155 156 /** 157 * Launches activity with the selected options. 158 */ 159 @Override launchActivity(Intent intent, boolean forResult)160 public void launchActivity(Intent intent, boolean forResult) { 161 if (forResult) { 162 startActivityForResult(intent, LAUNCH_FOR_RESULT_ID); 163 } else { 164 startActivity(intent); 165 } 166 167 // If people press back we want them to see the overview rather than the launch fragment. 168 // To achieve this we pop the launchFragment from the stack when we go to the next activity. 169 getSupportFragmentManager().popBackStack(); 170 } 171 172 @Override onNewIntent(Intent intent)173 protected void onNewIntent(Intent intent) { 174 super.onNewIntent(intent); 175 setIntent(intent); 176 } 177 178 @Override onCreateOptionsMenu(Menu menu)179 public boolean onCreateOptionsMenu(Menu menu) { 180 getMenuInflater().inflate(R.menu.app_bar, menu); 181 return true; 182 } 183 184 @Override onOptionsItemSelected(MenuItem item)185 public boolean onOptionsItemSelected(MenuItem item) { 186 switch (item.getItemId()) { 187 case R.id.app_bar_test: 188 runIntentTests(); 189 break; 190 case R.id.app_bar_launch_default: 191 askToLaunchTasks(); 192 break; 193 } 194 return super.onOptionsItemSelected(item); 195 } 196 askToLaunchTasks()197 private void askToLaunchTasks() { 198 AlertDialog dialog = new AlertDialog.Builder(this) 199 .setMessage(R.string.launch_explanation) 200 .setTitle(R.string.ask_to_launch) 201 .setPositiveButton(R.string.ask_to_launch_affirm, (dialogInterface, i) -> { 202 setupTaskPreset().startActivities(TestBase.LaunchStyle.TASK_STACK_BUILDER); 203 dialogInterface.dismiss(); 204 }) 205 .setNegativeButton(R.string.ask_to_launch_cancel, (dialogInterface, i) -> { 206 dialogInterface.dismiss(); 207 }) 208 .create(); 209 dialog.show(); 210 } 211 setupTaskPreset()212 protected TestBase setupTaskPreset() { 213 Node mRoot = Node.newRootNode(); 214 // Describe initial setup of tasks 215 // create singleTask, singleInstance, and two documents in separate tasks 216 Node singleTask = newTaskNode() 217 .addChild(new Node(new ComponentName(this, SingleTaskActivity.class))); 218 Node docLaunchAlways = newTaskNode() 219 .addChild(new Node(new ComponentName(this, DocumentLaunchAlwaysActivity.class))); 220 Node docLaunchInto = newTaskNode() 221 .addChild(new Node(new ComponentName(this, DocumentLaunchIntoActivity.class))); 222 // Create three t0asks with three activities each, with affinity set 223 Node taskAffinity1 = newTaskNode() 224 .addChild(new Node(new ComponentName(this, TaskAffinity1Activity.class))) 225 .addChild(new Node(new ComponentName(this, TaskAffinity1Activity.class))) 226 .addChild(new Node(new ComponentName(this, TaskAffinity1Activity.class))); 227 Node taskAffinity2 = newTaskNode() 228 .addChild(new Node(new ComponentName(this, TaskAffinity2Activity.class))) 229 .addChild(new Node(new ComponentName(this, TaskAffinity2Activity.class))) 230 .addChild(new Node(new ComponentName(this, TaskAffinity2Activity.class))); 231 Node taskAffinity3 = newTaskNode() 232 .addChild(new Node(new ComponentName(this, TaskAffinity3Activity.class))) 233 .addChild(new Node(new ComponentName(this, TaskAffinity3Activity.class))) 234 .addChild(new Node(new ComponentName(this, TaskAffinity3Activity.class))); 235 mRoot.addChild(singleTask).addChild(docLaunchAlways).addChild(docLaunchInto) 236 .addChild(taskAffinity1).addChild(taskAffinity2).addChild(taskAffinity3); 237 return new TestBase(this, mRoot); 238 } 239 runIntentTests()240 protected void runIntentTests() { 241 startActivity(getPackageManager() 242 .getLaunchIntentForPackage("com.example.android.intentplayground.test")); 243 } 244 prepareLaunchForward()245 protected Intent prepareLaunchForward() { 246 Intent intent = getIntent(); 247 Intent nextIntent = null; 248 if (intent.hasExtra(EXTRA_LAUNCH_FORWARD)) { 249 Log.e(getLocalClassName(), "It's happening! LAUNCH_FORWARD"); 250 ArrayList<Intent> intents = intent.getParcelableArrayListExtra(EXTRA_LAUNCH_FORWARD); 251 if (!intents.isEmpty()) { 252 nextIntent = intents.remove(0); 253 nextIntent.putParcelableArrayListExtra(EXTRA_LAUNCH_FORWARD, intents); 254 if (BuildConfig.DEBUG) { 255 Log.d(getLocalClassName(), EXTRA_LAUNCH_FORWARD + " " 256 + nextIntent.getComponent().toString()); 257 } 258 } 259 } 260 return nextIntent; 261 } 262 263 /** 264 * Sets a public field for the purpose of testing. 265 */ 266 @Override onUserLeaveHint()267 protected void onUserLeaveHint() { 268 super.onUserLeaveHint(); 269 userLeaveHintWasCalled = true; 270 } 271 } 272