1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.managedprovisioning.common;
18 
19 import static java.util.Objects.requireNonNull;
20 
21 import android.app.Activity;
22 import android.content.Intent;
23 import android.os.Bundle;
24 import android.os.Parcelable;
25 
26 import androidx.annotation.Nullable;
27 import androidx.lifecycle.ViewModelProvider;
28 
29 import com.android.managedprovisioning.R;
30 import com.android.managedprovisioning.common.RetryLaunchViewModel.Config;
31 import com.android.managedprovisioning.common.RetryLaunchViewModel.LaunchActivityEvent;
32 import com.android.managedprovisioning.common.RetryLaunchViewModel.RetryLaunchViewModelFactory;
33 
34 /**
35  * An {@link Activity} which tries to start the {@link #EXTRA_INTENT_TO_LAUNCH} intent
36  * {@link #EXTRA_MAX_RETRIES} times every {@link #EXTRA_RETRY_PERIOD_MS} milliseconds.
37  *
38  * <p>This {@link Activity} is meant to be used in cases where there is a possibility the {@link
39  * #EXTRA_INTENT_TO_LAUNCH} intent may not be available the first time, for example if the app
40  * that resolves the {@link Intent} is getting updated.
41  *
42  * <p>This {@link Activity} forwards the result code of the {@link Activity} that was resolved from
43  * the {@link #EXTRA_INTENT_TO_LAUNCH}. Upon failure to launch the {@link Intent}, this {@link
44  * Activity} returns {@link #RESULT_CANCELED}.
45  */
46 public final class RetryLaunchActivity extends SetupGlifLayoutActivity {
47 
48     /**
49      * A {@link Parcelable} extra describing the {@link Intent} to be launched.
50      *
51      * <p>This is a required extra. Not supplying it will throw an {@link IllegalStateException}.
52      */
53     public static final String EXTRA_INTENT_TO_LAUNCH =
54             "com.android.managedprovisioning.extra.INTENT_TO_LAUNCH";
55 
56     /**
57      * An {@code int} extra which determines how many times to retry the activity relaunch.
58      *
59      * <p>Must be a non-negative number.
60      *
61      * <p>Default value is {@code 3}.
62      */
63     public static final String EXTRA_MAX_RETRIES =
64             "com.android.managedprovisioning.extra.MAX_RETRIES";
65 
66     /**
67      * A {@code long} extra which determines how long to wait between each retry, in milliseconds.
68      *
69      * <p>Must be a non-negative number.
70      *
71      * <p>Default value is {@code 60000} (one minute).
72      */
73     public static final String EXTRA_RETRY_PERIOD_MS =
74             "com.android.managedprovisioning.extra.RETRY_PERIOD_MS";
75 
76     private static final int DEFAULT_MAX_RETRIES = 3;
77 
78     private static final long DEFAULT_RETRY_PERIOD_MS = 60_000L;
79 
80     private static final int LAUNCH_ACTIVITY_REQUEST_CODE = 1;
81 
82     private RetryLaunchViewModel mViewModel;
83 
84     @Override
onCreate(Bundle savedInstanceState)85     protected void onCreate(Bundle savedInstanceState) {
86         super.onCreate(savedInstanceState);
87         if (!getIntent().hasExtra(EXTRA_INTENT_TO_LAUNCH)) {
88             throw new IllegalStateException("EXTRA_INTENT must be supplied.");
89         }
90         Intent activityIntent = getIntent().getParcelableExtra(EXTRA_INTENT_TO_LAUNCH);
91         requireNonNull(activityIntent);
92 
93         Config config = createConfigFromIntent(getIntent());
94         mViewModel = createViewModelWithIntent(activityIntent, config);
95         setupViewModelObservation();
96 
97         if (savedInstanceState == null) {
98             mViewModel.tryStartActivity();
99         }
100     }
101 
createConfigFromIntent(Intent intent)102     private Config createConfigFromIntent(Intent intent) {
103         final int maxRetries = getMaxRetries(intent);
104         final long retryPeriodMs = getRetryPeriodMs(intent);
105         return new Config() {
106             @Override
107             public long getLaunchActivityRetryMillis() {
108                 return retryPeriodMs;
109             }
110 
111             @Override
112             public int getLaunchActivityMaxRetries() {
113                 return maxRetries;
114             }
115         };
116     }
117 
118     private int getMaxRetries(Intent intent) {
119         int maxRetries = intent.getIntExtra(EXTRA_MAX_RETRIES, DEFAULT_MAX_RETRIES);
120         if (maxRetries < 0) {
121             ProvisionLogger.loge("Invalid value passed for " + EXTRA_MAX_RETRIES + ". Expected a "
122                     + "non-negative value but got " + maxRetries);
123             maxRetries = DEFAULT_MAX_RETRIES;
124         }
125         return maxRetries;
126     }
127 
128     private long getRetryPeriodMs(Intent intent) {
129         long retryPeriodMs = intent.getLongExtra(EXTRA_RETRY_PERIOD_MS, DEFAULT_RETRY_PERIOD_MS);
130         if (retryPeriodMs < 0) {
131             ProvisionLogger.loge("Invalid value passed for " + EXTRA_RETRY_PERIOD_MS + ". Expected "
132                     + "a non-negative value but got " + retryPeriodMs);
133             retryPeriodMs = DEFAULT_RETRY_PERIOD_MS;
134         }
135         return retryPeriodMs;
136     }
137 
138     private void setupViewModelObservation() {
139         mViewModel.observeViewModelEvents().observe(this, viewModelEvent -> {
140             switch (viewModelEvent.getType()) {
141                 case RetryLaunchViewModel.VIEW_MODEL_EVENT_LAUNCH_ACTIVITY:
142                     if (!mViewModel.isWaitingForActivityResult()) {
143                         launchActivity(((LaunchActivityEvent) viewModelEvent).getIntent());
144                         mViewModel.markWaitingForActivityResult();
145                     }
146                     break;
147                 case RetryLaunchViewModel.VIEW_MODEL_EVENT_WAITING_FOR_RETRY:
148                     initializeUi();
149                     break;
150                 case RetryLaunchViewModel.VIEW_MODEL_EVENT_LAUNCH_FAILURE:
151                     finishWithCancelResult();
152                     break;
153             }
154         });
155     }
156 
157     private RetryLaunchViewModel createViewModelWithIntent(Intent activityIntent, Config config) {
158         return new ViewModelProvider(this,
159                 new RetryLaunchViewModelFactory(
160                         getApplication(),
161                         activityIntent,
162                         config,
163                         mUtils))
164                 .get(RetryLaunchViewModel.class);
165     }
166 
167     @Override
168     protected void onStop() {
169         super.onStop();
170         mViewModel.stopLaunchRetries();
171     }
172 
173     @Override
174     protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
175         if (requestCode != LAUNCH_ACTIVITY_REQUEST_CODE) {
176             super.onActivityResult(requestCode, resultCode, data);
177             return;
178         }
179         ProvisionLogger.logi("Retry launcher activity result code: " + resultCode);
180         finishWithResult(resultCode, data);
181     }
182 
183     private void initializeUi() {
184         int headerResId = R.string.just_a_sec;
185         int titleResId = R.string.just_a_sec;
186         initializeLayoutParams(R.layout.empty_loading_layout, headerResId);
187         setTitle(titleResId);
188     }
189 
190     private void launchActivity(Intent intent) {
191         getTransitionHelper().startActivityForResultWithTransition(
192                 this,
193                 intent,
194                 LAUNCH_ACTIVITY_REQUEST_CODE);
195     }
196 
197     private void finishWithResult(int resultCode, Intent data) {
198         setResult(resultCode, data);
199         getTransitionHelper().finishActivity(this);
200     }
201 
202     private void finishWithCancelResult() {
203         finishWithResult(RESULT_CANCELED, /*data=*/ null);
204     }
205 }
206