diff options
Diffstat (limited to 'setupwizardlib/src/main/java/com/android/setupwizardlib/util')
8 files changed, 1054 insertions, 0 deletions
diff --git a/setupwizardlib/src/main/java/com/android/setupwizardlib/util/AbstractRequireScrollHelper.java b/setupwizardlib/src/main/java/com/android/setupwizardlib/util/AbstractRequireScrollHelper.java new file mode 100644 index 0000000..2697371 --- /dev/null +++ b/setupwizardlib/src/main/java/com/android/setupwizardlib/util/AbstractRequireScrollHelper.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2016 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.android.setupwizardlib.util; + +import android.view.View; + +import com.android.setupwizardlib.view.NavigationBar; + +/** + * Add this helper to require the scroll view to be scrolled to the bottom, making sure that the + * user sees all content on the screen. This will change the navigation bar to show the more button + * instead of the next button when there is more content to be seen. When the more button is + * clicked, the scroll view will be scrolled one page down. + */ +public abstract class AbstractRequireScrollHelper implements View.OnClickListener { + + private final NavigationBar mNavigationBar; + + private boolean mScrollNeeded; + // Whether the user have seen the more button yet. + private boolean mScrollNotified = false; + + protected AbstractRequireScrollHelper(NavigationBar navigationBar) { + mNavigationBar = navigationBar; + } + + protected void requireScroll() { + mNavigationBar.getMoreButton().setOnClickListener(this); + } + + protected void notifyScrolledToBottom() { + if (mScrollNeeded) { + mNavigationBar.post(new Runnable() { + @Override + public void run() { + mNavigationBar.getNextButton().setVisibility(View.VISIBLE); + mNavigationBar.getMoreButton().setVisibility(View.GONE); + } + }); + mScrollNeeded = false; + mScrollNotified = true; + } + } + + protected void notifyRequiresScroll() { + if (!mScrollNeeded && !mScrollNotified) { + mNavigationBar.post(new Runnable() { + @Override + public void run() { + mNavigationBar.getNextButton().setVisibility(View.GONE); + mNavigationBar.getMoreButton().setVisibility(View.VISIBLE); + } + }); + mScrollNeeded = true; + } + } + + @Override + public void onClick(View view) { + pageScrollDown(); + } + + protected abstract void pageScrollDown(); +} diff --git a/setupwizardlib/src/main/java/com/android/setupwizardlib/util/DrawableLayoutDirectionHelper.java b/setupwizardlib/src/main/java/com/android/setupwizardlib/util/DrawableLayoutDirectionHelper.java new file mode 100644 index 0000000..bf4c0c2 --- /dev/null +++ b/setupwizardlib/src/main/java/com/android/setupwizardlib/util/DrawableLayoutDirectionHelper.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2015 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.android.setupwizardlib.util; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; +import android.os.Build; +import android.view.View; + +/** + * Provides convenience methods to handle drawable layout directions in different SDK versions. + */ +public class DrawableLayoutDirectionHelper { + + /** + * Creates an {@link android.graphics.drawable.InsetDrawable} according to the layout direction + * of {@code view}. + */ + public static InsetDrawable createRelativeInsetDrawable(Drawable drawable, + int insetStart, int insetTop, int insetEnd, int insetBottom, View view) { + boolean isRtl = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 + && view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + return createRelativeInsetDrawable(drawable, insetStart, insetTop, insetEnd, insetBottom, + isRtl); + } + + /** + * Creates an {@link android.graphics.drawable.InsetDrawable} according to the layout direction + * of {@code context}. + */ + public static InsetDrawable createRelativeInsetDrawable(Drawable drawable, + int insetStart, int insetTop, int insetEnd, int insetBottom, Context context) { + boolean isRtl = false; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + final int layoutDirection = + context.getResources().getConfiguration().getLayoutDirection(); + isRtl = layoutDirection == View.LAYOUT_DIRECTION_RTL; + } + return createRelativeInsetDrawable(drawable, insetStart, insetTop, insetEnd, insetBottom, + isRtl); + } + + /** + * Creates an {@link android.graphics.drawable.InsetDrawable} according to + * {@code layoutDirection}. + */ + public static InsetDrawable createRelativeInsetDrawable(Drawable drawable, + int insetStart, int insetTop, int insetEnd, int insetBottom, int layoutDirection) { + //noinspection AndroidLintInlinedApi + return createRelativeInsetDrawable(drawable, insetStart, insetTop, insetEnd, insetBottom, + layoutDirection == View.LAYOUT_DIRECTION_RTL); + } + + private static InsetDrawable createRelativeInsetDrawable(Drawable drawable, + int insetStart, int insetTop, int insetEnd, int insetBottom, boolean isRtl) { + if (isRtl) { + return new InsetDrawable(drawable, insetEnd, insetTop, insetStart, insetBottom); + } else { + return new InsetDrawable(drawable, insetStart, insetTop, insetEnd, insetBottom); + } + } +} diff --git a/setupwizardlib/src/main/java/com/android/setupwizardlib/util/ListViewRequireScrollHelper.java b/setupwizardlib/src/main/java/com/android/setupwizardlib/util/ListViewRequireScrollHelper.java new file mode 100644 index 0000000..7877569 --- /dev/null +++ b/setupwizardlib/src/main/java/com/android/setupwizardlib/util/ListViewRequireScrollHelper.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2016 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.android.setupwizardlib.util; + +import android.os.Build; +import android.widget.AbsListView; +import android.widget.ListAdapter; +import android.widget.ListView; + +import com.android.setupwizardlib.view.NavigationBar; + +/** + * Add this helper to require the list view to be scrolled to the bottom, making sure that the + * user sees all content on the screen. This will change the navigation bar to show the more button + * instead of the next button when there is more content to be seen. When the more button is + * clicked, the list view will be scrolled one page down. + */ +public class ListViewRequireScrollHelper extends AbstractRequireScrollHelper + implements AbsListView.OnScrollListener { + + public static void requireScroll(NavigationBar navigationBar, ListView listView) { + new ListViewRequireScrollHelper(navigationBar, listView).requireScroll(); + } + + private final ListView mListView; + + private ListViewRequireScrollHelper(NavigationBar navigationBar, ListView listView) { + super(navigationBar); + mListView = listView; + } + + @Override + protected void requireScroll() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { + // APIs to scroll a list only exists on Froyo or above. + super.requireScroll(); + mListView.setOnScrollListener(this); + + final ListAdapter adapter = mListView.getAdapter(); + if (mListView.getLastVisiblePosition() < adapter.getCount()) { + notifyRequiresScroll(); + } + } + } + + @Override + protected void pageScrollDown() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { + final int height = mListView.getHeight(); + mListView.smoothScrollBy(height, 500); + } + } + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + if (firstVisibleItem + visibleItemCount >= totalItemCount) { + notifyScrolledToBottom(); + } else { + notifyRequiresScroll(); + } + } +} diff --git a/setupwizardlib/src/main/java/com/android/setupwizardlib/util/Partner.java b/setupwizardlib/src/main/java/com/android/setupwizardlib/util/Partner.java new file mode 100644 index 0000000..fd187d9 --- /dev/null +++ b/setupwizardlib/src/main/java/com/android/setupwizardlib/util/Partner.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2015 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.android.setupwizardlib.util; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.util.Log; + +import com.android.setupwizardlib.annotations.VisibleForTesting; + +/** + * Utilities to discover and interact with partner customizations. An overlay package is one that + * registers the broadcast receiver for {@code com.android.setupwizard.action.PARTNER_CUSTOMIZATION} + * in its manifest. There can only be one customization APK on a device, and it must be bundled with + * the system. + * + * <p>Derived from {@code com.android.launcher3/Partner.java} + */ +public class Partner { + private static final String TAG = "(SUW) Partner"; + + /** Marker action used to discover partner */ + private static final String + ACTION_PARTNER_CUSTOMIZATION = "com.android.setupwizard.action.PARTNER_CUSTOMIZATION"; + + private static boolean sSearched = false; + private static Partner sPartner; + + /** + * Convenience to get a drawable from partner overlay, or if not available, the drawable from + * the original context. + * + * @see #getResourceEntry(android.content.Context, int) + */ + public static Drawable getDrawable(Context context, int id) { + final ResourceEntry entry = getResourceEntry(context, id); + return entry.resources.getDrawable(entry.id); + } + + /** + * Convenience to get a string from partner overlay, or if not available, the string from the + * original context. + * + * @see #getResourceEntry(android.content.Context, int) + */ + public static String getString(Context context, int id) { + final ResourceEntry entry = getResourceEntry(context, id); + return entry.resources.getString(entry.id); + } + + /** + * Find an entry of resource in the overlay package provided by partners. It will first look for + * the resource in the overlay package, and if not available, will return the one in the + * original context. + * + * @return a ResourceEntry in the partner overlay's resources, if one is defined. Otherwise the + * resources from the original context is returned. Clients can then get the resource by + * {@code entry.resources.getString(entry.id)}, or other methods available in + * {@link android.content.res.Resources}. + */ + public static ResourceEntry getResourceEntry(Context context, int id) { + final Partner partner = Partner.get(context); + if (partner != null) { + final Resources ourResources = context.getResources(); + final String name = ourResources.getResourceEntryName(id); + final String type = ourResources.getResourceTypeName(id); + final int partnerId = partner.getIdentifier(name, type); + if (partnerId != 0) { + return new ResourceEntry(partner.mResources, partnerId, true); + } + } + return new ResourceEntry(context.getResources(), id, false); + } + + public static class ResourceEntry { + public Resources resources; + public int id; + public boolean isOverlay; + + ResourceEntry(Resources resources, int id, boolean isOverlay) { + this.resources = resources; + this.id = id; + this.isOverlay = isOverlay; + } + } + + /** + * Find and return partner details, or {@code null} if none exists. A partner package is marked + * by a broadcast receiver declared in the manifest that handles the + * {@code com.android.setupwizard.action.PARTNER_CUSTOMIZATION} intent action. The overlay + * package must also be a system package. + */ + public static synchronized Partner get(Context context) { + if (!sSearched) { + PackageManager pm = context.getPackageManager(); + final Intent intent = new Intent(ACTION_PARTNER_CUSTOMIZATION); + for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) { + if (info.activityInfo == null) { + continue; + } + final ApplicationInfo appInfo = info.activityInfo.applicationInfo; + if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + try { + final Resources res = pm.getResourcesForApplication(appInfo); + sPartner = new Partner(appInfo.packageName, res); + break; + } catch (NameNotFoundException e) { + Log.w(TAG, "Failed to find resources for " + appInfo.packageName); + } + } + } + sSearched = true; + } + return sPartner; + } + + @VisibleForTesting + public static synchronized void resetForTesting() { + sSearched = false; + sPartner = null; + } + + private final String mPackageName; + private final Resources mResources; + + private Partner(String packageName, Resources res) { + mPackageName = packageName; + mResources = res; + } + + public String getPackageName() { + return mPackageName; + } + + public Resources getResources() { + return mResources; + } + + public int getIdentifier(String name, String defType) { + return mResources.getIdentifier(name, defType, mPackageName); + } +} diff --git a/setupwizardlib/src/main/java/com/android/setupwizardlib/util/RequireScrollHelper.java b/setupwizardlib/src/main/java/com/android/setupwizardlib/util/RequireScrollHelper.java new file mode 100644 index 0000000..cce336f --- /dev/null +++ b/setupwizardlib/src/main/java/com/android/setupwizardlib/util/RequireScrollHelper.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2015 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.android.setupwizardlib.util; + +import android.widget.ScrollView; + +import com.android.setupwizardlib.view.BottomScrollView; +import com.android.setupwizardlib.view.NavigationBar; + +/** + * Add this helper to require the scroll view to be scrolled to the bottom, making sure that the + * user sees all content on the screen. This will change the navigation bar to show the more button + * instead of the next button when there is more content to be seen. When the more button is + * clicked, the scroll view will be scrolled one page down. + */ +public class RequireScrollHelper extends AbstractRequireScrollHelper + implements BottomScrollView.BottomScrollListener { + + public static void requireScroll(NavigationBar navigationBar, BottomScrollView scrollView) { + new RequireScrollHelper(navigationBar, scrollView).requireScroll(); + } + + private final BottomScrollView mScrollView; + + private RequireScrollHelper(NavigationBar navigationBar, BottomScrollView scrollView) { + super(navigationBar); + mScrollView = scrollView; + } + + @Override + protected void requireScroll() { + super.requireScroll(); + mScrollView.setBottomScrollListener(this); + } + + @Override + protected void pageScrollDown() { + mScrollView.pageScroll(ScrollView.FOCUS_DOWN); + } + + @Override + public void onScrolledToBottom() { + notifyScrolledToBottom(); + } + + @Override + public void onRequiresScroll() { + notifyRequiresScroll(); + } +} diff --git a/setupwizardlib/src/main/java/com/android/setupwizardlib/util/ResultCodes.java b/setupwizardlib/src/main/java/com/android/setupwizardlib/util/ResultCodes.java new file mode 100644 index 0000000..a429e73 --- /dev/null +++ b/setupwizardlib/src/main/java/com/android/setupwizardlib/util/ResultCodes.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2015 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.android.setupwizardlib.util; + +import static android.app.Activity.RESULT_FIRST_USER; + +public final class ResultCodes { + + public static final int RESULT_SKIP = RESULT_FIRST_USER; + public static final int RESULT_RETRY = RESULT_FIRST_USER + 1; + public static final int RESULT_ACTIVITY_NOT_FOUND = RESULT_FIRST_USER + 2; + + public static final int RESULT_FIRST_SETUP_USER = RESULT_FIRST_USER + 100; +} diff --git a/setupwizardlib/src/main/java/com/android/setupwizardlib/util/SystemBarHelper.java b/setupwizardlib/src/main/java/com/android/setupwizardlib/util/SystemBarHelper.java new file mode 100644 index 0000000..44bcefc --- /dev/null +++ b/setupwizardlib/src/main/java/com/android/setupwizardlib/util/SystemBarHelper.java @@ -0,0 +1,348 @@ +/* + * Copyright (C) 2015 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.android.setupwizardlib.util; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.Dialog; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.Handler; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowInsets; +import android.view.WindowManager; + +import com.android.setupwizardlib.R; + +/** + * A helper class to manage the system navigation bar and status bar. This will add various + * systemUiVisibility flags to the given Window or View to make them follow the Setup Wizard style. + * + * When the useImmersiveMode intent extra is true, a screen in Setup Wizard should hide the system + * bars using methods from this class. For Lollipop, {@link #hideSystemBars(android.view.Window)} + * will completely hide the system navigation bar and change the status bar to transparent, and + * layout the screen contents (usually the illustration) behind it. + */ +public class SystemBarHelper { + + private static final String TAG = "SystemBarHelper"; + + @SuppressLint("InlinedApi") + private static final int DEFAULT_IMMERSIVE_FLAGS = + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; + + @SuppressLint("InlinedApi") + private static final int DIALOG_IMMERSIVE_FLAGS = + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + + /** + * Needs to be equal to View.STATUS_BAR_DISABLE_BACK + */ + private static final int STATUS_BAR_DISABLE_BACK = 0x00400000; + + /** + * The maximum number of retries when peeking the decor view. When polling for the decor view, + * waiting it to be installed, set a maximum number of retries. + */ + private static final int PEEK_DECOR_VIEW_RETRIES = 3; + + /** + * Hide the navigation bar for a dialog. + * + * <p>This will only take effect in versions Lollipop or above. Otherwise this is a no-op. + */ + public static void hideSystemBars(final Dialog dialog) { + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + final Window window = dialog.getWindow(); + temporarilyDisableDialogFocus(window); + addVisibilityFlag(window, DIALOG_IMMERSIVE_FLAGS); + addImmersiveFlagsToDecorView(window, DIALOG_IMMERSIVE_FLAGS); + + // Also set the navigation bar and status bar to transparent color. Note that this + // doesn't work if android.R.boolean.config_enableTranslucentDecor is false. + window.setNavigationBarColor(0); + window.setStatusBarColor(0); + } + } + + /** + * Hide the navigation bar, make the color of the status and navigation bars transparent, and + * specify {@link View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} flag so that the content is laid-out + * behind the transparent status bar. This is commonly used with + * {@link android.app.Activity#getWindow()} to make the navigation and status bars follow the + * Setup Wizard style. + * + * <p>This will only take effect in versions Lollipop or above. Otherwise this is a no-op. + */ + public static void hideSystemBars(final Window window) { + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + addVisibilityFlag(window, DEFAULT_IMMERSIVE_FLAGS); + addImmersiveFlagsToDecorView(window, DEFAULT_IMMERSIVE_FLAGS); + + // Also set the navigation bar and status bar to transparent color. Note that this + // doesn't work if android.R.boolean.config_enableTranslucentDecor is false. + window.setNavigationBarColor(0); + window.setStatusBarColor(0); + } + } + + /** + * Revert the actions of hideSystemBars. Note that this will remove the system UI visibility + * flags regardless of whether it is originally present. You should also manually reset the + * navigation bar and status bar colors, as this method doesn't know what value to revert it to. + */ + public static void showSystemBars(final Dialog dialog, final Context context) { + showSystemBars(dialog.getWindow(), context); + } + + /** + * Revert the actions of hideSystemBars. Note that this will remove the system UI visibility + * flags regardless of whether it is originally present. You should also manually reset the + * navigation bar and status bar colors, as this method doesn't know what value to revert it to. + */ + public static void showSystemBars(final Window window, final Context context) { + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + removeVisibilityFlag(window, DEFAULT_IMMERSIVE_FLAGS); + removeImmersiveFlagsFromDecorView(window, DEFAULT_IMMERSIVE_FLAGS); + + if (context != null) { + //noinspection AndroidLintInlinedApi + final TypedArray typedArray = context.obtainStyledAttributes(new int[]{ + android.R.attr.statusBarColor, android.R.attr.navigationBarColor}); + final int statusBarColor = typedArray.getColor(0, 0); + final int navigationBarColor = typedArray.getColor(1, 0); + window.setStatusBarColor(statusBarColor); + window.setNavigationBarColor(navigationBarColor); + typedArray.recycle(); + } + } + } + + /** + * Convenience method to add a visibility flag in addition to the existing ones. + */ + public static void addVisibilityFlag(final View view, final int flag) { + if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) { + final int vis = view.getSystemUiVisibility(); + view.setSystemUiVisibility(vis | flag); + } + } + + /** + * Convenience method to add a visibility flag in addition to the existing ones. + */ + public static void addVisibilityFlag(final Window window, final int flag) { + if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) { + WindowManager.LayoutParams attrs = window.getAttributes(); + attrs.systemUiVisibility |= flag; + window.setAttributes(attrs); + } + } + + /** + * Convenience method to remove a visibility flag from the view, leaving other flags that are + * not specified intact. + */ + public static void removeVisibilityFlag(final View view, final int flag) { + if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) { + final int vis = view.getSystemUiVisibility(); + view.setSystemUiVisibility(vis & ~flag); + } + } + + /** + * Convenience method to remove a visibility flag from the window, leaving other flags that are + * not specified intact. + */ + public static void removeVisibilityFlag(final Window window, final int flag) { + if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) { + WindowManager.LayoutParams attrs = window.getAttributes(); + attrs.systemUiVisibility &= ~flag; + window.setAttributes(attrs); + } + } + + public static void setBackButtonVisible(final Window window, final boolean visible) { + if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) { + if (visible) { + removeVisibilityFlag(window, STATUS_BAR_DISABLE_BACK); + } else { + addVisibilityFlag(window, STATUS_BAR_DISABLE_BACK); + } + } + } + + /** + * Set a view to be resized when the keyboard is shown. This will set the bottom margin of the + * view to be immediately above the keyboard, and assumes that the view sits immediately above + * the navigation bar. + * + * <p>Note that you must set {@link android.R.attr#windowSoftInputMode} to {@code adjustResize} + * for this class to work. Otherwise window insets are not dispatched and this method will have + * no effect. + * + * <p>This will only take effect in versions Lollipop or above. Otherwise this is a no-op. + * + * @param view The view to be resized when the keyboard is shown. + */ + public static void setImeInsetView(final View view) { + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + view.setOnApplyWindowInsetsListener(new WindowInsetsListener()); + } + } + + /** + * Add the specified immersive flags to the decor view of the window, because + * {@link View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} only takes effect when it is added to a view + * instead of the window. + */ + @TargetApi(VERSION_CODES.LOLLIPOP) + private static void addImmersiveFlagsToDecorView(final Window window, final int vis) { + getDecorView(window, new OnDecorViewInstalledListener() { + @Override + public void onDecorViewInstalled(View decorView) { + addVisibilityFlag(decorView, vis); + } + }); + } + + @TargetApi(VERSION_CODES.LOLLIPOP) + private static void removeImmersiveFlagsFromDecorView(final Window window, final int vis) { + getDecorView(window, new OnDecorViewInstalledListener() { + @Override + public void onDecorViewInstalled(View decorView) { + removeVisibilityFlag(decorView, vis); + } + }); + } + + private static void getDecorView(Window window, OnDecorViewInstalledListener callback) { + new DecorViewFinder().getDecorView(window, callback, PEEK_DECOR_VIEW_RETRIES); + } + + private static class DecorViewFinder { + + private final Handler mHandler = new Handler(); + private Window mWindow; + private int mRetries; + private OnDecorViewInstalledListener mCallback; + + private Runnable mCheckDecorViewRunnable = new Runnable() { + @Override + public void run() { + // Use peekDecorView instead of getDecorView so that clients can still set window + // features after calling this method. + final View decorView = mWindow.peekDecorView(); + if (decorView != null) { + mCallback.onDecorViewInstalled(decorView); + } else { + mRetries--; + if (mRetries >= 0) { + // If the decor view is not installed yet, try again in the next loop. + mHandler.post(mCheckDecorViewRunnable); + } else { + Log.w(TAG, "Cannot get decor view of window: " + mWindow); + } + } + } + }; + + public void getDecorView(Window window, OnDecorViewInstalledListener callback, + int retries) { + mWindow = window; + mRetries = retries; + mCallback = callback; + mCheckDecorViewRunnable.run(); + } + } + + private interface OnDecorViewInstalledListener { + + void onDecorViewInstalled(View decorView); + } + + /** + * Apply a hack to temporarily set the window to not focusable, so that the navigation bar + * will not show up during the transition. + */ + private static void temporarilyDisableDialogFocus(final Window window) { + window.setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); + // Add the SOFT_INPUT_IS_FORWARD_NAVIGATION_FLAG. This is normally done by the system when + // FLAG_NOT_FOCUSABLE is not set. Setting this flag allows IME to be shown automatically + // if the dialog has editable text fields. + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION); + new Handler().post(new Runnable() { + @Override + public void run() { + window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); + } + }); + } + + @TargetApi(VERSION_CODES.LOLLIPOP) + private static class WindowInsetsListener implements View.OnApplyWindowInsetsListener { + private int mBottomOffset; + private boolean mHasCalculatedBottomOffset = false; + + @Override + public WindowInsets onApplyWindowInsets(View view, WindowInsets insets) { + if (!mHasCalculatedBottomOffset) { + mBottomOffset = getBottomDistance(view); + mHasCalculatedBottomOffset = true; + } + + int bottomInset = insets.getSystemWindowInsetBottom(); + + final int bottomMargin = Math.max( + insets.getSystemWindowInsetBottom() - mBottomOffset, 0); + + final ViewGroup.MarginLayoutParams lp = + (ViewGroup.MarginLayoutParams) view.getLayoutParams(); + // Check that we have enough space to apply the bottom margins before applying it. + // Otherwise the framework may think that the view is empty and exclude it from layout. + if (bottomMargin < lp.bottomMargin + view.getHeight()) { + lp.setMargins(lp.leftMargin, lp.topMargin, lp.rightMargin, bottomMargin); + view.setLayoutParams(lp); + bottomInset = 0; + } + + + return insets.replaceSystemWindowInsets( + insets.getSystemWindowInsetLeft(), + insets.getSystemWindowInsetTop(), + insets.getSystemWindowInsetRight(), + bottomInset + ); + } + } + + private static int getBottomDistance(View view) { + int[] coords = new int[2]; + view.getLocationInWindow(coords); + return view.getRootView().getHeight() - coords[1] - view.getHeight(); + } +} diff --git a/setupwizardlib/src/main/java/com/android/setupwizardlib/util/WizardManagerHelper.java b/setupwizardlib/src/main/java/com/android/setupwizardlib/util/WizardManagerHelper.java new file mode 100644 index 0000000..10172ce --- /dev/null +++ b/setupwizardlib/src/main/java/com/android/setupwizardlib/util/WizardManagerHelper.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2015 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.android.setupwizardlib.util; + +import android.content.Context; +import android.content.Intent; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.provider.Settings; + +public class WizardManagerHelper { + + private static final String ACTION_NEXT = "com.android.wizard.NEXT"; + + /* + * EXTRA_SCRIPT_URI and EXTRA_ACTION_ID will be removed once all outstanding references have + * transitioned to using EXTRA_WIZARD_BUNDLE. + */ + @Deprecated + private static final String EXTRA_SCRIPT_URI = "scriptUri"; + @Deprecated + private static final String EXTRA_ACTION_ID = "actionId"; + + private static final String EXTRA_WIZARD_BUNDLE = "wizardBundle"; + private static final String EXTRA_RESULT_CODE = "com.android.setupwizard.ResultCode"; + private static final String EXTRA_IS_FIRST_RUN = "firstRun"; + + public static final String EXTRA_THEME = "theme"; + public static final String EXTRA_USE_IMMERSIVE_MODE = "useImmersiveMode"; + + public static final String SETTINGS_GLOBAL_DEVICE_PROVISIONED = "device_provisioned"; + public static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete"; + + public static final String THEME_HOLO = "holo"; + public static final String THEME_HOLO_LIGHT = "holo_light"; + public static final String THEME_MATERIAL = "material"; + public static final String THEME_MATERIAL_LIGHT = "material_light"; + + /** + * @deprecated This constant is not used and will not be passed by any released version of setup + * wizard. + */ + @Deprecated + public static final String THEME_MATERIAL_BLUE = "material_blue"; + + /** + * @deprecated This constant is not used and will not be passed by any released version of setup + * wizard. + */ + @Deprecated + public static final String THEME_MATERIAL_BLUE_LIGHT = "material_blue_light"; + + /** + * Passed in a setup wizard intent as {@link #EXTRA_THEME}. This is the dark variant of the + * theme used in setup wizard for NYC. + */ + public static final String THEME_GLIF = "glif"; + + /** + * Passed in a setup wizard intent as {@link #EXTRA_THEME}. This is the default theme used in + * setup wizard for NYC. + */ + public static final String THEME_GLIF_LIGHT = "glif_light"; + + /** + * Get an intent that will invoke the next step of setup wizard. + * + * @param originalIntent The original intent that was used to start the step, usually via + * {@link android.app.Activity#getIntent()}. + * @param resultCode The result code of the step. See {@link ResultCodes}. + * @return A new intent that can be used with + * {@link android.app.Activity#startActivityForResult(Intent, int)} to start the next + * step of the setup flow. + */ + public static Intent getNextIntent(Intent originalIntent, int resultCode) { + return getNextIntent(originalIntent, resultCode, null); + } + + /** + * Get an intent that will invoke the next step of setup wizard. + * + * @param originalIntent The original intent that was used to start the step, usually via + * {@link android.app.Activity#getIntent()}. + * @param resultCode The result code of the step. See {@link ResultCodes}. + * @param data An intent containing extra result data. + * @return A new intent that can be used with + * {@link android.app.Activity#startActivityForResult(Intent, int)} to start the next + * step of the setup flow. + */ + public static Intent getNextIntent(Intent originalIntent, int resultCode, Intent data) { + Intent intent = new Intent(ACTION_NEXT); + copyWizardManagerExtras(originalIntent, intent); + intent.putExtra(EXTRA_RESULT_CODE, resultCode); + if (data != null && data.getExtras() != null) { + intent.putExtras(data.getExtras()); + } + intent.putExtra(EXTRA_THEME, originalIntent.getStringExtra(EXTRA_THEME)); + + return intent; + } + + /** + * Copy the internal extras used by setup wizard from one intent to another. For low-level use + * only, such as when using {@link Intent#FLAG_ACTIVITY_FORWARD_RESULT} to relay to another + * intent. + * + * @param srcIntent Intent to get the wizard manager extras from. + * @param dstIntent Intent to copy the wizard manager extras to. + */ + public static void copyWizardManagerExtras(Intent srcIntent, Intent dstIntent) { + dstIntent.putExtra(EXTRA_WIZARD_BUNDLE, srcIntent.getBundleExtra(EXTRA_WIZARD_BUNDLE)); + dstIntent.putExtra(EXTRA_IS_FIRST_RUN, + srcIntent.getBooleanExtra(EXTRA_IS_FIRST_RUN, false)); + dstIntent.putExtra(EXTRA_SCRIPT_URI, srcIntent.getStringExtra(EXTRA_SCRIPT_URI)); + dstIntent.putExtra(EXTRA_ACTION_ID, srcIntent.getStringExtra(EXTRA_ACTION_ID)); + } + + /** + * Check whether an intent is intended to be used within the setup wizard flow. + * + * @param intent The intent to be checked, usually from + * {@link android.app.Activity#getIntent()}. + * @return true if the intent passed in was intended to be used with setup wizard. + */ + public static boolean isSetupWizardIntent(Intent intent) { + return intent.getBooleanExtra(EXTRA_IS_FIRST_RUN, false); + } + + /** + * Checks whether the current user has completed Setup Wizard. This is true if the current user + * has gone through Setup Wizard. The current user may or may not be the device owner and the + * device owner may have already completed setup wizard. + * + * @param context The context to retrieve the settings. + * @return true if the current user has completed Setup Wizard. + * @see #isDeviceProvisioned(android.content.Context) + */ + public static boolean isUserSetupComplete(Context context) { + if (VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH) { + return Settings.Secure.getInt(context.getContentResolver(), + SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; + } else { + // For versions below JB MR1, there are no user profiles. Just return the global device + // provisioned state. + return Settings.Secure.getInt(context.getContentResolver(), + SETTINGS_GLOBAL_DEVICE_PROVISIONED, 0) == 1; + } + } + + /** + * Checks whether the device is provisioned. This means that the device has gone through Setup + * Wizard at least once. Note that the user can still be in Setup Wizard even if this is true, + * for a secondary user profile triggered through Settings > Add account. + * + * @param context The context to retrieve the settings. + * @return true if the device is provisioned. + * @see #isUserSetupComplete(android.content.Context) + */ + public static boolean isDeviceProvisioned(Context context) { + if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) { + return Settings.Global.getInt(context.getContentResolver(), + SETTINGS_GLOBAL_DEVICE_PROVISIONED, 0) == 1; + } else { + return Settings.Secure.getInt(context.getContentResolver(), + SETTINGS_GLOBAL_DEVICE_PROVISIONED, 0) == 1; + } + } + + /** + * Checks the intent whether the extra indicates that the light theme should be used or not. If + * the theme is not specified in the intent, or the theme specified is unknown, the value def + * will be returned. + * + * @param intent The intent used to start the activity, which the theme extra will be read from. + * @param def The default value if the theme is not specified. + * @return True if the activity started by the given intent should use light theme. + */ + public static boolean isLightTheme(Intent intent, boolean def) { + final String theme = intent.getStringExtra(EXTRA_THEME); + return isLightTheme(theme, def); + } + + /** + * Checks whether {@code theme} represents a light or dark theme. If the theme specified is + * unknown, the value def will be returned. + * + * @param theme The theme as specified from an intent sent from setup wizard. + * @param def The default value if the theme is not known. + * @return True if {@code theme} represents a light theme. + */ + public static boolean isLightTheme(String theme, boolean def) { + if (THEME_HOLO_LIGHT.equals(theme) || THEME_MATERIAL_LIGHT.equals(theme) + || THEME_MATERIAL_BLUE_LIGHT.equals(theme) || THEME_GLIF_LIGHT.equals(theme)) { + return true; + } else if (THEME_HOLO.equals(theme) || THEME_MATERIAL.equals(theme) + || THEME_MATERIAL_BLUE.equals(theme) || THEME_GLIF.equals(theme)) { + return false; + } else { + return def; + } + } +} |