1 /*
2  * Copyright (C) 2018 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.settings.core;
18 
19 import android.annotation.StringRes;
20 import android.content.ComponentName;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.os.Bundle;
24 import android.os.UserHandle;
25 import android.text.TextUtils;
26 
27 import androidx.annotation.NonNull;
28 import androidx.annotation.VisibleForTesting;
29 import androidx.fragment.app.Fragment;
30 
31 import com.android.settings.SettingsActivity;
32 import com.android.settings.SubSettings;
33 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
34 import com.android.settingslib.transition.SettingsTransitionHelper.TransitionType;
35 
36 public class SubSettingLauncher {
37 
38     private final Context mContext;
39     private final LaunchRequest mLaunchRequest;
40     private boolean mLaunched;
41 
SubSettingLauncher(Context context)42     public SubSettingLauncher(Context context) {
43         if (context == null) {
44             throw new IllegalArgumentException("Context must be non-null.");
45         }
46         mContext = context;
47         mLaunchRequest = new LaunchRequest();
48         mLaunchRequest.mTransitionType = TransitionType.TRANSITION_SHARED_AXIS;
49     }
50 
setDestination(String fragmentName)51     public SubSettingLauncher setDestination(String fragmentName) {
52         mLaunchRequest.mDestinationName = fragmentName;
53         return this;
54     }
55 
56     /**
57      * Set title with resource string id.
58      *
59      * @param titleResId res id of string
60      */
setTitleRes(@tringRes int titleResId)61     public SubSettingLauncher setTitleRes(@StringRes int titleResId) {
62         return setTitleRes(null /*titlePackageName*/, titleResId);
63     }
64 
65     /**
66      * Set title with resource string id, and package name to resolve the resource id.
67      *
68      * @param titlePackageName package name to resolve resource
69      * @param titleResId       res id of string, will use package name to resolve
70      */
setTitleRes(String titlePackageName, @StringRes int titleResId)71     public SubSettingLauncher setTitleRes(String titlePackageName, @StringRes int titleResId) {
72         mLaunchRequest.mTitleResPackageName = titlePackageName;
73         mLaunchRequest.mTitleResId = titleResId;
74         mLaunchRequest.mTitle = null;
75         return this;
76     }
77 
78     /**
79      * Set title with text,
80      * This method is only for user generated string,
81      * display text will not update after locale change,
82      * if title string is from resource id, please use setTitleRes.
83      *
84      * @param title text title
85      */
setTitleText(CharSequence title)86     public SubSettingLauncher setTitleText(CharSequence title) {
87         mLaunchRequest.mTitle = title;
88         return this;
89     }
90 
setArguments(Bundle arguments)91     public SubSettingLauncher setArguments(Bundle arguments) {
92         mLaunchRequest.mArguments = arguments;
93         return this;
94     }
95 
setExtras(Bundle extras)96     public SubSettingLauncher setExtras(Bundle extras) {
97         mLaunchRequest.mExtras = extras;
98         return this;
99     }
100 
setSourceMetricsCategory(int sourceMetricsCategory)101     public SubSettingLauncher setSourceMetricsCategory(int sourceMetricsCategory) {
102         mLaunchRequest.mSourceMetricsCategory = sourceMetricsCategory;
103         return this;
104     }
105 
setResultListener(Fragment listener, int resultRequestCode)106     public SubSettingLauncher setResultListener(Fragment listener, int resultRequestCode) {
107         mLaunchRequest.mRequestCode = resultRequestCode;
108         mLaunchRequest.mResultListener = listener;
109         return this;
110     }
111 
addFlags(int flags)112     public SubSettingLauncher addFlags(int flags) {
113         mLaunchRequest.mFlags |= flags;
114         return this;
115     }
116 
setUserHandle(UserHandle userHandle)117     public SubSettingLauncher setUserHandle(UserHandle userHandle) {
118         mLaunchRequest.mUserHandle = userHandle;
119         return this;
120     }
121 
setTransitionType(int transitionType)122     public SubSettingLauncher setTransitionType(int transitionType) {
123         mLaunchRequest.mTransitionType = transitionType;
124         return this;
125     }
126 
127     /** Decide whether the next page is second layer page or not. */
setIsSecondLayerPage(boolean isSecondLayerPage)128     public SubSettingLauncher setIsSecondLayerPage(boolean isSecondLayerPage) {
129         mLaunchRequest.mIsSecondLayerPage = isSecondLayerPage;
130         return this;
131     }
132 
launch()133     public void launch() {
134         launchWithIntent(toIntent());
135     }
136 
137     /**
138      * Launch sub settings activity with an intent.
139      *
140      * @param intent the settings intent we want to launch
141      */
launchWithIntent(@onNull Intent intent)142     public void launchWithIntent(@NonNull Intent intent) {
143         verifyIntent(intent);
144         if (mLaunched) {
145             throw new IllegalStateException(
146                     "This launcher has already been executed. Do not reuse");
147         }
148         mLaunched = true;
149 
150         boolean launchAsUser = mLaunchRequest.mUserHandle != null
151                 && mLaunchRequest.mUserHandle.getIdentifier() != UserHandle.myUserId();
152         boolean launchForResult = mLaunchRequest.mResultListener != null;
153         if (launchAsUser && launchForResult) {
154             launchForResultAsUser(intent, mLaunchRequest.mUserHandle,
155                     mLaunchRequest.mResultListener, mLaunchRequest.mRequestCode);
156         } else if (launchAsUser && !launchForResult) {
157             launchAsUser(intent, mLaunchRequest.mUserHandle);
158         } else if (!launchAsUser && launchForResult) {
159             launchForResult(mLaunchRequest.mResultListener, intent, mLaunchRequest.mRequestCode);
160         } else {
161             launch(intent);
162         }
163     }
164 
165     /**
166      * Verify intent is correctly constructed.
167      *
168      * @param intent the intent to verify
169      */
170     @VisibleForTesting
verifyIntent(@onNull Intent intent)171     public void verifyIntent(@NonNull Intent intent) {
172         String className = SubSettings.class.getName();
173         ComponentName componentName = intent.getComponent();
174         String destinationName = intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT);
175         int sourceMetricsCategory =
176                 intent.getIntExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, -1);
177 
178         if (componentName != null && !TextUtils.equals(className, componentName.getClassName())) {
179             throw new IllegalArgumentException(String.format("Class must be: %s", className));
180         } else if (TextUtils.isEmpty(destinationName)) {
181             throw new IllegalArgumentException("Destination fragment must be set");
182         } else if (sourceMetricsCategory < 0) {
183             throw new IllegalArgumentException("Source metrics category must be set");
184         }
185     }
186 
toIntent()187     public Intent toIntent() {
188         final Intent intent = new Intent(Intent.ACTION_MAIN);
189         copyExtras(intent);
190         intent.setClass(mContext, SubSettings.class);
191         if (TextUtils.isEmpty(mLaunchRequest.mDestinationName)) {
192             throw new IllegalArgumentException("Destination fragment must be set");
193         }
194         intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT, mLaunchRequest.mDestinationName);
195 
196         if (mLaunchRequest.mSourceMetricsCategory < 0) {
197             throw new IllegalArgumentException("Source metrics category must be set");
198         }
199         intent.putExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY,
200                 mLaunchRequest.mSourceMetricsCategory);
201 
202         intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, mLaunchRequest.mArguments);
203         intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RES_PACKAGE_NAME,
204                 mLaunchRequest.mTitleResPackageName);
205         intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RESID,
206                 mLaunchRequest.mTitleResId);
207         intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE, mLaunchRequest.mTitle);
208         intent.addFlags(mLaunchRequest.mFlags);
209         intent.putExtra(SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE,
210                 mLaunchRequest.mTransitionType);
211         intent.putExtra(SettingsActivity.EXTRA_IS_SECOND_LAYER_PAGE,
212                 mLaunchRequest.mIsSecondLayerPage);
213 
214         return intent;
215     }
216 
217     @VisibleForTesting
launch(Intent intent)218     void launch(Intent intent) {
219         mContext.startActivity(intent);
220     }
221 
222     @VisibleForTesting
launchAsUser(Intent intent, UserHandle userHandle)223     void launchAsUser(Intent intent, UserHandle userHandle) {
224         mContext.startActivityAsUser(intent, userHandle);
225     }
226 
227     @VisibleForTesting
launchForResultAsUser(Intent intent, UserHandle userHandle, Fragment resultListener, int requestCode)228     void launchForResultAsUser(Intent intent, UserHandle userHandle,
229             Fragment resultListener, int requestCode) {
230         resultListener.getActivity().startActivityForResultAsUser(intent, requestCode, userHandle);
231     }
232 
233     @VisibleForTesting
launchForResult(Fragment listener, Intent intent, int requestCode)234     void launchForResult(Fragment listener, Intent intent, int requestCode) {
235         listener.startActivityForResult(intent, requestCode);
236     }
237 
copyExtras(Intent intent)238     private void copyExtras(Intent intent) {
239         if (mLaunchRequest.mExtras != null) {
240             intent.replaceExtras(mLaunchRequest.mExtras);
241         }
242     }
243 
244     /**
245      * Simple container that has information about how to launch a subsetting.
246      */
247     static class LaunchRequest {
248         String mDestinationName;
249         int mTitleResId;
250         String mTitleResPackageName;
251         CharSequence mTitle;
252         int mSourceMetricsCategory = -100;
253         int mFlags;
254         Fragment mResultListener;
255         int mRequestCode;
256         UserHandle mUserHandle;
257         int mTransitionType;
258         Bundle mArguments;
259         Bundle mExtras;
260         boolean mIsSecondLayerPage;
261     }
262 }
263