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