summaryrefslogtreecommitdiffstats
path: root/src/com/android/packageinstaller/permission/ui/wear
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/packageinstaller/permission/ui/wear')
-rw-r--r--src/com/android/packageinstaller/permission/ui/wear/AppPermissionsFragmentWear.java335
-rw-r--r--src/com/android/packageinstaller/permission/ui/wear/ConfirmationViewHandler.java381
-rw-r--r--src/com/android/packageinstaller/permission/ui/wear/TitledSettingsFragment.java234
-rw-r--r--src/com/android/packageinstaller/permission/ui/wear/WarningConfirmationActivity.java118
-rw-r--r--src/com/android/packageinstaller/permission/ui/wear/settings/ExtendedOnCenterProximityListener.java30
-rw-r--r--src/com/android/packageinstaller/permission/ui/wear/settings/ExtendedViewHolder.java84
-rw-r--r--src/com/android/packageinstaller/permission/ui/wear/settings/PermissionsSettingsAdapter.java101
-rw-r--r--src/com/android/packageinstaller/permission/ui/wear/settings/SettingsAdapter.java276
-rw-r--r--src/com/android/packageinstaller/permission/ui/wear/settings/ViewUtils.java48
9 files changed, 1607 insertions, 0 deletions
diff --git a/src/com/android/packageinstaller/permission/ui/wear/AppPermissionsFragmentWear.java b/src/com/android/packageinstaller/permission/ui/wear/AppPermissionsFragmentWear.java
new file mode 100644
index 00000000..aba97fc8
--- /dev/null
+++ b/src/com/android/packageinstaller/permission/ui/wear/AppPermissionsFragmentWear.java
@@ -0,0 +1,335 @@
+/*
+* 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.packageinstaller.permission.ui.wear;
+
+import android.Manifest;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.support.wearable.view.WearableListView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import com.android.packageinstaller.R;
+import com.android.packageinstaller.permission.model.AppPermissionGroup;
+import com.android.packageinstaller.permission.model.AppPermissions;
+import com.android.packageinstaller.permission.ui.OverlayTouchActivity;
+import com.android.packageinstaller.permission.ui.wear.settings.PermissionsSettingsAdapter;
+import com.android.packageinstaller.permission.ui.wear.settings.SettingsAdapter;
+import com.android.packageinstaller.permission.utils.LocationUtils;
+import com.android.packageinstaller.permission.utils.SafetyNetLogger;
+import com.android.packageinstaller.permission.utils.Utils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public final class AppPermissionsFragmentWear extends TitledSettingsFragment {
+
+ private static final String LOG_TAG = "ManagePermsFragment";
+
+ private static final int WARNING_CONFIRMATION_REQUEST = 252;
+ private List<AppPermissionGroup> mToggledGroups;
+ private AppPermissions mAppPermissions;
+ private PermissionsSettingsAdapter mAdapter;
+
+ private boolean mHasConfirmedRevoke;
+
+ public static AppPermissionsFragmentWear newInstance(String packageName) {
+ return setPackageName(new AppPermissionsFragmentWear(), packageName);
+ }
+
+ private static <T extends Fragment> T setPackageName(T fragment, String packageName) {
+ Bundle arguments = new Bundle();
+ arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName);
+ fragment.setArguments(arguments);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ String packageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME);
+ Activity activity = getActivity();
+ PackageManager pm = activity.getPackageManager();
+ PackageInfo packageInfo;
+
+ try {
+ packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.i(LOG_TAG, "No package:" + activity.getCallingPackage(), e);
+ packageInfo = null;
+ }
+
+ if (packageInfo == null) {
+ Toast.makeText(activity, R.string.app_not_found_dlg_title, Toast.LENGTH_LONG).show();
+ activity.finish();
+ return;
+ }
+
+ mAppPermissions = new AppPermissions(activity, packageInfo, null, true, new Runnable() {
+ @Override
+ public void run() {
+ getActivity().finish();
+ }
+ });
+
+ mAdapter = new PermissionsSettingsAdapter(getContext());
+
+ initializePermissionGroupList();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.settings, container, false);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mAppPermissions.refresh();
+
+ // Also refresh the UI
+ final int count = mAdapter.getItemCount();
+ for (int i = 0; i < count; ++i) {
+ updatePermissionGroupSetting(i);
+ }
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ if (mAppPermissions != null) {
+ initializeLayout(mAdapter);
+ bindHeader(mAppPermissions.getPackageInfo());
+ }
+ }
+
+ private void bindHeader(PackageInfo packageInfo) {
+ Activity activity = getActivity();
+ PackageManager pm = activity.getPackageManager();
+ ApplicationInfo appInfo = packageInfo.applicationInfo;
+ CharSequence label = appInfo.loadLabel(pm);
+ mHeader.setText(label);
+ }
+
+ private void initializePermissionGroupList() {
+ final String packageName = mAppPermissions.getPackageInfo().packageName;
+ List<AppPermissionGroup> groups = mAppPermissions.getPermissionGroups();
+ List<SettingsAdapter.Setting<AppPermissionGroup>> nonSystemGroups = new ArrayList<>();
+
+ final int count = groups.size();
+ for (int i = 0; i < count; ++i) {
+ final AppPermissionGroup group = groups.get(i);
+ if (!Utils.shouldShowPermission(group, packageName)) {
+ continue;
+ }
+
+ boolean isPlatform = group.getDeclaringPackage().equals(Utils.OS_PKG);
+
+ SettingsAdapter.Setting<AppPermissionGroup> setting =
+ new SettingsAdapter.Setting<AppPermissionGroup>(
+ group.getLabel(),
+ getPermissionGroupIcon(group),
+ i);
+ setting.data = group;
+
+ // The UI shows System settings first, then non-system settings
+ if (isPlatform) {
+ mAdapter.addSetting(setting);
+ } else {
+ nonSystemGroups.add(setting);
+ }
+ }
+
+ // Now add the non-system settings to the end of the list
+ final int nonSystemCount = nonSystemGroups.size();
+ for (int i = 0; i < nonSystemCount; ++i) {
+ final SettingsAdapter.Setting<AppPermissionGroup> setting = nonSystemGroups.get(i);
+ mAdapter.addSetting(setting);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ logAndClearToggledGroups();
+ }
+
+ @Override
+ public void onClick(WearableListView.ViewHolder view) {
+ final int index = view.getPosition();
+ SettingsAdapter.Setting<AppPermissionGroup> setting = mAdapter.get(index);
+ final AppPermissionGroup group = setting.data;
+
+ if (group == null) {
+ Log.e(LOG_TAG, "Error: AppPermissionGroup is null");
+ return;
+ }
+
+ // The way WearableListView is designed, there is no way to avoid this click handler
+ // Since the policy is fixed, ignore the click as the user is not able to change the state
+ // of this permission group
+ if (group.isPolicyFixed()) {
+ return;
+ }
+
+ OverlayTouchActivity activity = (OverlayTouchActivity) getActivity();
+ if (activity.isObscuredTouch()) {
+ activity.showOverlayDialog();
+ return;
+ }
+
+ addToggledGroup(group);
+
+ if (LocationUtils.isLocationGroupAndProvider(group.getName(), group.getApp().packageName)) {
+ LocationUtils.showLocationDialog(getContext(), mAppPermissions.getAppLabel());
+ return;
+ }
+
+ if (!group.areRuntimePermissionsGranted()) {
+ group.grantRuntimePermissions(false);
+ } else {
+ final boolean grantedByDefault = group.hasGrantedByDefaultPermission();
+ if (grantedByDefault || (!group.hasRuntimePermission() && !mHasConfirmedRevoke)) {
+ Intent intent = new Intent(getActivity(), WarningConfirmationActivity.class);
+ intent.putExtra(WarningConfirmationActivity.EXTRA_WARNING_MESSAGE,
+ getString(grantedByDefault ?
+ R.string.system_warning : R.string.old_sdk_deny_warning));
+ intent.putExtra(WarningConfirmationActivity.EXTRA_INDEX, index);
+ startActivityForResult(intent, WARNING_CONFIRMATION_REQUEST);
+ } else {
+ group.revokeRuntimePermissions(false);
+ }
+ }
+
+ updatePermissionGroupSetting(index);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == WARNING_CONFIRMATION_REQUEST) {
+ if (resultCode == Activity.RESULT_OK) {
+ int index = data.getIntExtra(WarningConfirmationActivity.EXTRA_INDEX, -1);
+ if (index == -1) {
+ Log.e(LOG_TAG, "Warning confirmation request came back with no index.");
+ return;
+ }
+
+ SettingsAdapter.Setting<AppPermissionGroup> setting = mAdapter.get(index);
+ final AppPermissionGroup group = setting.data;
+ group.revokeRuntimePermissions(false);
+ if (!group.hasGrantedByDefaultPermission()) {
+ mHasConfirmedRevoke = true;
+ }
+
+ updatePermissionGroupSetting(index);
+ }
+ } else {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+ private void updatePermissionGroupSetting(int index) {
+ SettingsAdapter.Setting<AppPermissionGroup> setting = mAdapter.get(index);
+ AppPermissionGroup group = setting.data;
+ mAdapter.updateSetting(
+ index,
+ group.getLabel(),
+ getPermissionGroupIcon(group),
+ group);
+ }
+
+ private void addToggledGroup(AppPermissionGroup group) {
+ if (mToggledGroups == null) {
+ mToggledGroups = new ArrayList<>();
+ }
+ // Double toggle is back to initial state.
+ if (mToggledGroups.contains(group)) {
+ mToggledGroups.remove(group);
+ } else {
+ mToggledGroups.add(group);
+ }
+ }
+
+ private void logAndClearToggledGroups() {
+ if (mToggledGroups != null) {
+ String packageName = mAppPermissions.getPackageInfo().packageName;
+ SafetyNetLogger.logPermissionsToggled(packageName, mToggledGroups);
+ mToggledGroups = null;
+ }
+ }
+
+ private int getPermissionGroupIcon(AppPermissionGroup group) {
+ String groupName = group.getName();
+ boolean isEnabled = group.areRuntimePermissionsGranted();
+ int resId;
+
+ switch (groupName) {
+ case Manifest.permission_group.CALENDAR:
+ resId = isEnabled ? R.drawable.ic_permission_calendar
+ : R.drawable.ic_permission_calendardisable;
+ break;
+ case Manifest.permission_group.CAMERA:
+ resId = isEnabled ? R.drawable.ic_permission_camera
+ : R.drawable.ic_permission_cameradisable;
+ break;
+ case Manifest.permission_group.CONTACTS:
+ resId = isEnabled ? R.drawable.ic_permission_contact
+ : R.drawable.ic_permission_contactdisable;
+ break;
+ case Manifest.permission_group.LOCATION:
+ resId = isEnabled ? R.drawable.ic_permission_location
+ : R.drawable.ic_permission_locationdisable;
+ break;
+ case Manifest.permission_group.MICROPHONE:
+ resId = isEnabled ? R.drawable.ic_permission_mic
+ : R.drawable.ic_permission_micdisable;
+ break;
+ case Manifest.permission_group.PHONE:
+ resId = isEnabled ? R.drawable.ic_permission_call
+ : R.drawable.ic_permission_calldisable;
+ break;
+ case Manifest.permission_group.SENSORS:
+ resId = isEnabled ? R.drawable.ic_permission_sensor
+ : R.drawable.ic_permission_sensordisable;
+ break;
+ case Manifest.permission_group.SMS:
+ resId = isEnabled ? R.drawable.ic_permission_sms
+ : R.drawable.ic_permission_smsdisable;
+ break;
+ case Manifest.permission_group.STORAGE:
+ resId = isEnabled ? R.drawable.ic_permission_storage
+ : R.drawable.ic_permission_storagedisable;
+ break;
+ default:
+ resId = isEnabled ? R.drawable.ic_permission_shield
+ : R.drawable.ic_permission_shielddisable;
+ }
+
+ return resId;
+ }
+}
diff --git a/src/com/android/packageinstaller/permission/ui/wear/ConfirmationViewHandler.java b/src/com/android/packageinstaller/permission/ui/wear/ConfirmationViewHandler.java
new file mode 100644
index 00000000..1c55e1bd
--- /dev/null
+++ b/src/com/android/packageinstaller/permission/ui/wear/ConfirmationViewHandler.java
@@ -0,0 +1,381 @@
+package com.android.packageinstaller.permission.ui.wear;
+
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import com.android.packageinstaller.R;
+
+public abstract class ConfirmationViewHandler implements
+ Handler.Callback,
+ View.OnClickListener,
+ ViewTreeObserver.OnScrollChangedListener,
+ ViewTreeObserver.OnGlobalLayoutListener {
+ private static final String TAG = "ConfirmationViewHandler";
+
+ public static final int MODE_HORIZONTAL_BUTTONS = 0;
+ public static final int MODE_VERTICAL_BUTTONS = 1;
+
+ private static final int MSG_SHOW_BUTTON_BAR = 1001;
+ private static final int MSG_HIDE_BUTTON_BAR = 1002;
+ private static final long HIDE_ANIM_DURATION = 500;
+
+ private View mRoot;
+ private TextView mCurrentPageText;
+ private ImageView mIcon;
+ private TextView mMessage;
+ private ScrollView mScrollingContainer;
+ private ViewGroup mContent;
+ private ViewGroup mHorizontalButtonBar;
+ private ViewGroup mVerticalButtonBar;
+ private Button mVerticalButton1;
+ private Button mVerticalButton2;
+ private Button mVerticalButton3;
+ private View mButtonBarContainer;
+
+ private Context mContext;
+
+ private Handler mHideHandler;
+ private Interpolator mInterpolator;
+ private float mButtonBarFloatingHeight;
+ private ObjectAnimator mButtonBarAnimator;
+ private float mCurrentTranslation;
+ private boolean mHiddenBefore;
+
+ // TODO: Move these into a builder
+ /** In the 2 button layout, this is allow button */
+ public abstract void onButton1();
+ /** In the 2 button layout, this is deny button */
+ public abstract void onButton2();
+ public abstract void onButton3();
+ public abstract CharSequence getVerticalButton1Text();
+ public abstract CharSequence getVerticalButton2Text();
+ public abstract CharSequence getVerticalButton3Text();
+ public abstract Drawable getVerticalButton1Icon();
+ public abstract Drawable getVerticalButton2Icon();
+ public abstract Drawable getVerticalButton3Icon();
+ public abstract CharSequence getCurrentPageText();
+ public abstract Icon getPermissionIcon();
+ public abstract CharSequence getMessage();
+
+ public ConfirmationViewHandler(Context context) {
+ mContext = context;
+ }
+
+ public View createView() {
+ mRoot = LayoutInflater.from(mContext).inflate(R.layout.confirmation_dialog, null);
+
+ mMessage = (TextView) mRoot.findViewById(R.id.message);
+ mCurrentPageText = (TextView) mRoot.findViewById(R.id.current_page_text);
+ mIcon = (ImageView) mRoot.findViewById(R.id.icon);
+ mButtonBarContainer = mRoot.findViewById(R.id.button_bar_container);
+ mContent = (ViewGroup) mRoot.findViewById(R.id.content);
+ mScrollingContainer = (ScrollView) mRoot.findViewById(R.id.scrolling_container);
+ mHorizontalButtonBar = (ViewGroup) mRoot.findViewById(R.id.horizontal_button_bar);
+ mVerticalButtonBar = (ViewGroup) mRoot.findViewById(R.id.vertical_button_bar);
+
+ Button horizontalAllow = (Button) mRoot.findViewById(R.id.permission_allow_button);
+ Button horizontalDeny = (Button) mRoot.findViewById(R.id.permission_deny_button);
+ horizontalAllow.setOnClickListener(this);
+ horizontalDeny.setOnClickListener(this);
+
+ mVerticalButton1 = (Button) mRoot.findViewById(R.id.vertical_button1);
+ mVerticalButton2 = (Button) mRoot.findViewById(R.id.vertical_button2);
+ mVerticalButton3 = (Button) mRoot.findViewById(R.id.vertical_button3);
+ mVerticalButton1.setOnClickListener(this);
+ mVerticalButton2.setOnClickListener(this);
+ mVerticalButton3.setOnClickListener(this);
+
+ mInterpolator = AnimationUtils.loadInterpolator(mContext,
+ android.R.interpolator.fast_out_slow_in);
+ mButtonBarFloatingHeight = mContext.getResources().getDimension(
+ R.dimen.conf_diag_floating_height);
+ mHideHandler = new Handler(Looper.getMainLooper(), this);
+
+ mScrollingContainer.getViewTreeObserver().addOnScrollChangedListener(this);
+ mRoot.getViewTreeObserver().addOnGlobalLayoutListener(this);
+
+ return mRoot;
+ }
+
+ /**
+ * Child class should override this for other modes. Call invalidate() to update the UI to the
+ * new button mode.
+ * @return The current mode the layout should use for the buttons
+ */
+ public int getButtonBarMode() {
+ return MODE_HORIZONTAL_BUTTONS;
+ }
+
+ public void invalidate() {
+ CharSequence currentPageText = getCurrentPageText();
+ if (!TextUtils.isEmpty(currentPageText)) {
+ mCurrentPageText.setText(currentPageText);
+ mCurrentPageText.setVisibility(View.VISIBLE);
+ } else {
+ mCurrentPageText.setVisibility(View.GONE);
+ }
+
+ Icon icon = getPermissionIcon();
+ if (icon != null) {
+ mIcon.setImageIcon(icon);
+ mIcon.setVisibility(View.VISIBLE);
+ } else {
+ mIcon.setVisibility(View.GONE);
+ }
+ mMessage.setText(getMessage());
+
+ switch (getButtonBarMode()) {
+ case MODE_HORIZONTAL_BUTTONS:
+ mHorizontalButtonBar.setVisibility(View.VISIBLE);
+ mVerticalButtonBar.setVisibility(View.GONE);
+ break;
+ case MODE_VERTICAL_BUTTONS:
+ mHorizontalButtonBar.setVisibility(View.GONE);
+ mVerticalButtonBar.setVisibility(View.VISIBLE);
+
+ mVerticalButton1.setText(getVerticalButton1Text());
+ mVerticalButton2.setText(getVerticalButton2Text());
+
+ mVerticalButton1.setCompoundDrawablesWithIntrinsicBounds(
+ getVerticalButton1Icon(), null, null, null);
+ mVerticalButton2.setCompoundDrawablesWithIntrinsicBounds(
+ getVerticalButton2Icon(), null, null, null);
+
+ CharSequence verticalButton3Text = getVerticalButton3Text();
+ if (TextUtils.isEmpty(verticalButton3Text)) {
+ mVerticalButton3.setVisibility(View.GONE);
+ } else {
+ mVerticalButton3.setText(getVerticalButton3Text());
+ mVerticalButton3.setCompoundDrawablesWithIntrinsicBounds(
+ getVerticalButton3Icon(), null, null, null);
+ }
+ break;
+ }
+
+ mScrollingContainer.scrollTo(0, 0);
+
+ mHideHandler.removeMessages(MSG_HIDE_BUTTON_BAR);
+ mHideHandler.removeMessages(MSG_SHOW_BUTTON_BAR);
+ }
+
+ @Override
+ public void onGlobalLayout() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onGlobalLayout");
+ Log.d(TAG, " contentHeight: " + mContent.getHeight());
+ }
+
+ if (mButtonBarAnimator != null) {
+ mButtonBarAnimator.cancel();
+ }
+
+ // In order to fake the buttons peeking at the bottom, need to do set the
+ // padding properly.
+ if (mContent.getPaddingBottom() != mButtonBarContainer.getHeight()) {
+ mContent.setPadding(mContent.getPaddingLeft(), mContent.getPaddingTop(),
+ mContent.getPaddingRight(), mButtonBarContainer.getHeight());
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, " set mContent.PaddingBottom: " + mButtonBarContainer.getHeight());
+ }
+ }
+
+ mButtonBarContainer.setTranslationY(mButtonBarContainer.getHeight());
+
+ // Give everything a chance to render
+ mHideHandler.removeMessages(MSG_HIDE_BUTTON_BAR);
+ mHideHandler.removeMessages(MSG_SHOW_BUTTON_BAR);
+ mHideHandler.sendEmptyMessageDelayed(MSG_SHOW_BUTTON_BAR, 50);
+ }
+
+ @Override
+ public void onClick(View v) {
+ int id = v.getId();
+ switch (id) {
+ case R.id.permission_allow_button:
+ case R.id.vertical_button1:
+ onButton1();
+ break;
+ case R.id.permission_deny_button:
+ case R.id.vertical_button2:
+ onButton2();
+ break;
+ case R.id.vertical_button3:
+ onButton3();
+ break;
+ }
+ }
+
+ @Override
+ public boolean handleMessage (Message msg) {
+ switch (msg.what) {
+ case MSG_SHOW_BUTTON_BAR:
+ showButtonBar();
+ return true;
+ case MSG_HIDE_BUTTON_BAR:
+ hideButtonBar();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onScrollChanged () {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onScrollChanged");
+ }
+ mHideHandler.removeMessages(MSG_HIDE_BUTTON_BAR);
+ hideButtonBar();
+ }
+
+ private void showButtonBar() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "showButtonBar");
+ }
+
+ // Setup Button animation.
+ // pop the button bar back to full height, stop all animation
+ if (mButtonBarAnimator != null) {
+ mButtonBarAnimator.cancel();
+ }
+
+ // stop any calls to hide the button bar in the future
+ mHideHandler.removeMessages(MSG_HIDE_BUTTON_BAR);
+ mHiddenBefore = false;
+
+ // Evaluate the max height the button bar can go
+ final int screenHeight = mRoot.getHeight();
+ final int halfScreenHeight = screenHeight / 2;
+ final int buttonBarHeight = mButtonBarContainer.getHeight();
+ final int contentHeight = mContent.getHeight() - buttonBarHeight;
+ final int buttonBarMaxHeight =
+ Math.min(buttonBarHeight, halfScreenHeight);
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, " screenHeight: " + screenHeight);
+ Log.d(TAG, " contentHeight: " + contentHeight);
+ Log.d(TAG, " buttonBarHeight: " + buttonBarHeight);
+ Log.d(TAG, " buttonBarMaxHeight: " + buttonBarMaxHeight);
+ }
+
+ mButtonBarContainer.setTranslationZ(mButtonBarFloatingHeight);
+
+ // Only hide the button bar if it is occluding the content or the button bar is bigger than
+ // half the screen
+ if (contentHeight > (screenHeight - buttonBarHeight)
+ || buttonBarHeight > halfScreenHeight) {
+ mHideHandler.sendEmptyMessageDelayed(MSG_HIDE_BUTTON_BAR, 3000);
+ }
+
+ generateButtonBarAnimator(buttonBarHeight,
+ buttonBarHeight - buttonBarMaxHeight, 0, mButtonBarFloatingHeight, 1000);
+ }
+
+ private void hideButtonBar() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "hideButtonBar");
+ }
+
+ // The desired margin space between the button bar and the bottom of the dialog text
+ final int topMargin = mContext.getResources().getDimensionPixelSize(
+ R.dimen.conf_diag_button_container_top_margin);
+ final int contentHeight = mContent.getHeight() + topMargin;
+ final int screenHeight = mRoot.getHeight();
+ final int buttonBarHeight = mButtonBarContainer.getHeight();
+
+ final int offset = screenHeight + buttonBarHeight
+ - contentHeight + Math.max(mScrollingContainer.getScrollY(), 0);
+ final int translationY = (offset > 0 ?
+ mButtonBarContainer.getHeight() - offset : mButtonBarContainer.getHeight());
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, " topMargin: " + topMargin);
+ Log.d(TAG, " contentHeight: " + contentHeight);
+ Log.d(TAG, " screenHeight: " + screenHeight);
+ Log.d(TAG, " offset: " + offset);
+ Log.d(TAG, " buttonBarHeight: " + buttonBarHeight);
+ Log.d(TAG, " mContent.getPaddingBottom(): " + mContent.getPaddingBottom());
+ Log.d(TAG, " mScrollingContainer.getScrollY(): " + mScrollingContainer.getScrollY());
+ Log.d(TAG, " translationY: " + translationY);
+ }
+
+ if (!mHiddenBefore || mButtonBarAnimator == null) {
+ // Remove previous call to MSG_SHOW_BUTTON_BAR if the user scrolled or something before
+ // the animation got a chance to play
+ mHideHandler.removeMessages(MSG_SHOW_BUTTON_BAR);
+
+ if(mButtonBarAnimator != null) {
+ mButtonBarAnimator.cancel(); // stop current animation if there is one playing
+ }
+
+ // hasn't hidden the bar yet, just hide now to the right height
+ generateButtonBarAnimator(
+ mButtonBarContainer.getTranslationY(), translationY,
+ mButtonBarFloatingHeight, 0, HIDE_ANIM_DURATION);
+ } else if (mButtonBarAnimator.isRunning()) {
+ // we are animating the button bar closing, change to animate to the right place
+ if (Math.abs(mCurrentTranslation - translationY) > 1e-2f) {
+ mButtonBarAnimator.cancel(); // stop current animation
+
+ if (Math.abs(mButtonBarContainer.getTranslationY() - translationY) > 1e-2f) {
+ long duration = Math.max((long) (
+ (float) HIDE_ANIM_DURATION
+ * (translationY - mButtonBarContainer.getTranslationY())
+ / mButtonBarContainer.getHeight()), 0);
+
+ generateButtonBarAnimator(
+ mButtonBarContainer.getTranslationY(), translationY,
+ mButtonBarFloatingHeight, 0, duration);
+ } else {
+ mButtonBarContainer.setTranslationY(translationY);
+ mButtonBarContainer.setTranslationZ(0);
+ }
+ }
+ } else {
+ // not currently animating, have already hidden, snap to the right offset
+ mButtonBarContainer.setTranslationY(translationY);
+ mButtonBarContainer.setTranslationZ(0);
+ }
+
+ mHiddenBefore = true;
+ }
+
+ private void generateButtonBarAnimator(
+ float startY, float endY, float startZ, float endZ, long duration) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "generateButtonBarAnimator");
+ Log.d(TAG, " startY: " + startY);
+ Log.d(TAG, " endY: " + endY);
+ Log.d(TAG, " startZ: " + startZ);
+ Log.d(TAG, " endZ: " + endZ);
+ Log.d(TAG, " duration: " + duration);
+ }
+
+ mButtonBarAnimator =
+ ObjectAnimator.ofPropertyValuesHolder(
+ mButtonBarContainer,
+ PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, startY, endY),
+ PropertyValuesHolder.ofFloat(View.TRANSLATION_Z, startZ, endZ));
+ mCurrentTranslation = endY;
+ mButtonBarAnimator.setDuration(duration);
+ mButtonBarAnimator.setInterpolator(mInterpolator);
+ mButtonBarAnimator.start();
+ }
+}
diff --git a/src/com/android/packageinstaller/permission/ui/wear/TitledSettingsFragment.java b/src/com/android/packageinstaller/permission/ui/wear/TitledSettingsFragment.java
new file mode 100644
index 00000000..ef7efb28
--- /dev/null
+++ b/src/com/android/packageinstaller/permission/ui/wear/TitledSettingsFragment.java
@@ -0,0 +1,234 @@
+/*
+ * 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.packageinstaller.permission.ui.wear;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v7.widget.RecyclerView;
+import android.support.wearable.view.WearableListView;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowInsets;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.packageinstaller.permission.ui.wear.settings.ViewUtils;
+import com.android.packageinstaller.R;
+
+/**
+ * Base settings Fragment that shows a title at the top of the page.
+ */
+public abstract class TitledSettingsFragment extends Fragment implements
+ View.OnLayoutChangeListener, WearableListView.ClickListener {
+
+ private static final int ITEM_CHANGE_DURATION_MS = 120;
+
+ private static final String TAG = "TitledSettingsFragment";
+ private int mInitialHeaderHeight;
+
+ protected TextView mHeader;
+ protected WearableListView mWheel;
+
+ private int mCharLimitShortTitle;
+ private int mCharLimitLine;
+ private int mChinOffset;
+
+ private TextWatcher mHeaderTextWatcher = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ adjustHeaderSize();
+ }
+ };
+
+ private void adjustHeaderTranslation() {
+ int translation = 0;
+ if (mWheel.getChildCount() > 0) {
+ translation = mWheel.getCentralViewTop() - mWheel.getChildAt(0).getTop();
+ }
+
+ float newTranslation = Math.min(Math.max(-mInitialHeaderHeight, -translation), 0);
+
+ int position = mWheel.getChildAdapterPosition(mWheel.getChildAt(0));
+ if (position == 0 || newTranslation < 0) {
+ mHeader.setTranslationY(newTranslation);
+ }
+ }
+
+ @Override
+ public void onTopEmptyRegionClick() {
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mCharLimitShortTitle = getResources().getInteger(R.integer.short_title_length);
+ mCharLimitLine = getResources().getInteger(R.integer.char_limit_per_line);
+ }
+
+ @Override
+ public void onLayoutChange(View view, int left, int top, int right, int bottom, int oldLeft,
+ int oldTop, int oldRight, int oldBottom) {
+ if (view == mHeader) {
+ mInitialHeaderHeight = bottom - top;
+ if (ViewUtils.getIsCircular(getContext())) {
+ // We are adding more margin on circular screens, so we need to account for it and use
+ // it for hiding the header.
+ mInitialHeaderHeight +=
+ ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).topMargin;
+ }
+ } else if (view == mWheel) {
+ adjustHeaderTranslation();
+ }
+ }
+
+ protected void initializeLayout(RecyclerView.Adapter adapter) {
+ View v = getView();
+ mWheel = (WearableListView) v.findViewById(R.id.wheel);
+
+ mHeader = (TextView) v.findViewById(R.id.header);
+ mHeader.addOnLayoutChangeListener(this);
+ mHeader.addTextChangedListener(mHeaderTextWatcher);
+
+ mWheel.setAdapter(adapter);
+ mWheel.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ }
+
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ adjustHeaderTranslation();
+ }
+ });
+ mWheel.setClickListener(this);
+ mWheel.addOnLayoutChangeListener(this);
+
+ // Decrease item change animation duration to approximately half of the default duration.
+ RecyclerView.ItemAnimator itemAnimator = mWheel.getItemAnimator();
+ itemAnimator.setChangeDuration(ITEM_CHANGE_DURATION_MS);
+
+ adjustHeaderSize();
+
+ positionOnCircular(getContext(), mHeader, mWheel);
+ }
+
+ public void positionOnCircular(Context context, View header, final ViewGroup wheel) {
+ if (ViewUtils.getIsCircular(context)) {
+ FrameLayout.LayoutParams params =
+ (FrameLayout.LayoutParams) header.getLayoutParams();
+ params.topMargin = (int) context.getResources().getDimension(
+ R.dimen.settings_header_top_margin_circular);
+ // Note that the margins are made symmetrical here. Since they're symmetrical we choose
+ // the smaller value to maximize usable width.
+ final int margin = (int) Math.min(context.getResources().getDimension(
+ R.dimen.round_content_padding_left), context.getResources().getDimension(
+ R.dimen.round_content_padding_right));
+ params.leftMargin = margin;
+ params.rightMargin = margin;
+ params.gravity = Gravity.CENTER_HORIZONTAL;
+ header.setLayoutParams(params);
+
+ if (header instanceof TextView) {
+ ((TextView) header).setGravity(Gravity.CENTER);
+ }
+
+ final int leftPadding = (int) context.getResources().getDimension(
+ R.dimen.round_content_padding_left);
+ final int rightPadding = (int) context.getResources().getDimension(
+ R.dimen.round_content_padding_right);
+ final int topPadding = (int) context.getResources().getDimension(
+ R.dimen.settings_wearable_list_view_vertical_padding_round);
+ final int bottomPadding = (int) context.getResources().getDimension(
+ R.dimen.settings_wearable_list_view_vertical_padding_round);
+ wheel.setPadding(leftPadding, topPadding, rightPadding, mChinOffset + bottomPadding);
+ wheel.setClipToPadding(false);
+
+ wheel.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
+ @Override
+ public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
+ mChinOffset = insets.getSystemWindowInsetBottom();
+ wheel.setPadding(leftPadding, topPadding, rightPadding,
+ mChinOffset + bottomPadding);
+ // This listener is invoked after each time we navigate to SettingsActivity and
+ // it keeps adding padding. We need to disable it after the first update.
+ v.setOnApplyWindowInsetsListener(null);
+ return insets.consumeSystemWindowInsets();
+ }
+ });
+ } else {
+ int leftPadding = (int) context.getResources().getDimension(
+ R.dimen.content_padding_left);
+ wheel.setPadding(leftPadding, wheel.getPaddingTop(), wheel.getPaddingRight(),
+ wheel.getPaddingBottom());
+ }
+ }
+
+ private void adjustHeaderSize() {
+ int length = mHeader.length();
+
+ if (length <= mCharLimitShortTitle) {
+ mHeader.setTextSize(TypedValue.COMPLEX_UNIT_PX,
+ getResources().getDimensionPixelSize(
+ R.dimen.setting_short_header_text_size));
+ } else {
+ mHeader.setTextSize(TypedValue.COMPLEX_UNIT_PX,
+ getResources().getDimensionPixelSize(
+ R.dimen.setting_long_header_text_size));
+ }
+
+ boolean singleLine = length <= mCharLimitLine;
+
+ float height = getResources().getDimension(R.dimen.settings_header_base_height);
+ if (!singleLine) {
+ height += getResources().getDimension(R.dimen.setting_header_extra_line_height);
+ }
+ mHeader.setMinHeight((int) height);
+
+ FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mHeader.getLayoutParams();
+ final Context context = getContext();
+ if (!singleLine) {
+ // Make the top margin a little bit smaller so there is more space for the title.
+ if (ViewUtils.getIsCircular(context)) {
+ params.topMargin = getResources().getDimensionPixelSize(
+ R.dimen.settings_header_top_margin_circular_multiline);
+ } else {
+ params.topMargin = getResources().getDimensionPixelSize(
+ R.dimen.settings_header_top_margin_multiline);
+ }
+ } else {
+ if (ViewUtils.getIsCircular(context)) {
+ params.topMargin = getResources().getDimensionPixelSize(
+ R.dimen.settings_header_top_margin_circular);
+ } else {
+ params.topMargin = getResources().getDimensionPixelSize(
+ R.dimen.settings_header_top_margin);
+ }
+ }
+ mHeader.setLayoutParams(params);
+ }
+}
diff --git a/src/com/android/packageinstaller/permission/ui/wear/WarningConfirmationActivity.java b/src/com/android/packageinstaller/permission/ui/wear/WarningConfirmationActivity.java
new file mode 100644
index 00000000..03713419
--- /dev/null
+++ b/src/com/android/packageinstaller/permission/ui/wear/WarningConfirmationActivity.java
@@ -0,0 +1,118 @@
+/*
+* 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.packageinstaller.permission.ui.wear;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+
+import com.android.packageinstaller.R;
+
+public final class WarningConfirmationActivity extends Activity {
+ public final static String EXTRA_WARNING_MESSAGE = "EXTRA_WARNING_MESSAGE";
+ // Saved index that will be returned in the onActivityResult() callback
+ public final static String EXTRA_INDEX = "EXTRA_INDEX";
+
+ private ConfirmationViewHandler mViewHandler;
+ private String mMessage;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mMessage = getIntent().getStringExtra(EXTRA_WARNING_MESSAGE);
+
+ mViewHandler = new ConfirmationViewHandler(this) {
+ @Override // ConfirmationViewHandler
+ public int getButtonBarMode() {
+ return MODE_VERTICAL_BUTTONS;
+ }
+
+ @Override
+ public void onButton1() {
+ setResultAndFinish(Activity.RESULT_CANCELED);
+ }
+
+ @Override
+ public void onButton2() {
+ setResultAndFinish(Activity.RESULT_OK);
+ }
+
+ @Override
+ public void onButton3() {
+ // no-op
+ }
+
+ @Override
+ public CharSequence getVerticalButton1Text() {
+ return getString(R.string.cancel);
+ }
+
+ @Override
+ public CharSequence getVerticalButton2Text() {
+ return getString(R.string.grant_dialog_button_deny);
+ }
+
+ @Override
+ public CharSequence getVerticalButton3Text() {
+ return null;
+ }
+
+ @Override
+ public Drawable getVerticalButton1Icon() {
+ return getDrawable(R.drawable.cancel_button);
+ }
+
+ @Override
+ public Drawable getVerticalButton2Icon() {
+ return getDrawable(R.drawable.confirm_button);
+ }
+
+ @Override
+ public Drawable getVerticalButton3Icon() {
+ return null;
+ }
+
+ @Override
+ public CharSequence getCurrentPageText() {
+ return null;
+ }
+
+ @Override
+ public Icon getPermissionIcon() {
+ return null;
+ }
+
+ @Override
+ public CharSequence getMessage() {
+ return mMessage;
+ }
+ };
+
+ setContentView(mViewHandler.createView());
+ mViewHandler.invalidate();
+ }
+
+ private void setResultAndFinish(int result) {
+ Intent intent = new Intent();
+ intent.putExtra(EXTRA_INDEX, getIntent().getIntExtra(EXTRA_INDEX, -1));
+ setResult(result, intent);
+ finish();
+ }
+}
diff --git a/src/com/android/packageinstaller/permission/ui/wear/settings/ExtendedOnCenterProximityListener.java b/src/com/android/packageinstaller/permission/ui/wear/settings/ExtendedOnCenterProximityListener.java
new file mode 100644
index 00000000..02c203b3
--- /dev/null
+++ b/src/com/android/packageinstaller/permission/ui/wear/settings/ExtendedOnCenterProximityListener.java
@@ -0,0 +1,30 @@
+/*
+ * 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.packageinstaller.permission.ui.wear.settings;
+
+import android.support.wearable.view.WearableListView;
+
+public interface ExtendedOnCenterProximityListener
+ extends WearableListView.OnCenterProximityListener {
+ float getProximityMinValue();
+
+ float getProximityMaxValue();
+
+ float getCurrentProximityValue();
+
+ void setScalingAnimatorValue(float value);
+}
diff --git a/src/com/android/packageinstaller/permission/ui/wear/settings/ExtendedViewHolder.java b/src/com/android/packageinstaller/permission/ui/wear/settings/ExtendedViewHolder.java
new file mode 100644
index 00000000..6b725419
--- /dev/null
+++ b/src/com/android/packageinstaller/permission/ui/wear/settings/ExtendedViewHolder.java
@@ -0,0 +1,84 @@
+/*
+ * 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.packageinstaller.permission.ui.wear.settings;
+
+import android.animation.ObjectAnimator;
+import android.support.wearable.view.WearableListView;
+import android.view.View;
+
+
+public class ExtendedViewHolder extends WearableListView.ViewHolder {
+ public static final long DEFAULT_ANIMATION_DURATION = 150;
+
+ private ObjectAnimator mScalingUpAnimator;
+
+ private ObjectAnimator mScalingDownAnimator;
+
+ private float mMinValue;
+
+ private float mMaxValue;
+
+ public ExtendedViewHolder(View itemView) {
+ super(itemView);
+ if (itemView instanceof ExtendedOnCenterProximityListener) {
+ ExtendedOnCenterProximityListener item =
+ (ExtendedOnCenterProximityListener) itemView;
+ mMinValue = item.getProximityMinValue();
+ item.setScalingAnimatorValue(mMinValue);
+ mMaxValue = item.getProximityMaxValue();
+ mScalingUpAnimator = ObjectAnimator.ofFloat(item, "scalingAnimatorValue", mMinValue,
+ mMaxValue);
+ mScalingUpAnimator.setDuration(DEFAULT_ANIMATION_DURATION);
+ mScalingDownAnimator = ObjectAnimator.ofFloat(item, "scalingAnimatorValue",
+ mMaxValue, mMinValue);
+ mScalingDownAnimator.setDuration(DEFAULT_ANIMATION_DURATION);
+ }
+ }
+
+ public void onCenterProximity(boolean isCentralItem, boolean animate) {
+ if (!(itemView instanceof ExtendedOnCenterProximityListener)) {
+ return;
+ }
+ ExtendedOnCenterProximityListener item = (ExtendedOnCenterProximityListener) itemView;
+ if (isCentralItem) {
+ if (animate) {
+ mScalingDownAnimator.cancel();
+ if (!mScalingUpAnimator.isRunning()) {
+ mScalingUpAnimator.setFloatValues(item.getCurrentProximityValue(),
+ mMaxValue);
+ mScalingUpAnimator.start();
+ }
+ } else {
+ mScalingUpAnimator.cancel();
+ item.setScalingAnimatorValue(item.getProximityMaxValue());
+ }
+ } else {
+ mScalingUpAnimator.cancel();
+ if (animate) {
+ if (!mScalingDownAnimator.isRunning()) {
+ mScalingDownAnimator.setFloatValues(item.getCurrentProximityValue(),
+ mMinValue);
+ mScalingDownAnimator.start();
+ }
+ } else {
+ mScalingDownAnimator.cancel();
+ item.setScalingAnimatorValue(item.getProximityMinValue());
+ }
+ }
+ super.onCenterProximity(isCentralItem, animate);
+ }
+}
diff --git a/src/com/android/packageinstaller/permission/ui/wear/settings/PermissionsSettingsAdapter.java b/src/com/android/packageinstaller/permission/ui/wear/settings/PermissionsSettingsAdapter.java
new file mode 100644
index 00000000..0e0adcbb
--- /dev/null
+++ b/src/com/android/packageinstaller/permission/ui/wear/settings/PermissionsSettingsAdapter.java
@@ -0,0 +1,101 @@
+/*
+ * 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.packageinstaller.permission.ui.wear.settings;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.support.wearable.view.WearableListView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.packageinstaller.R;
+import com.android.packageinstaller.permission.model.AppPermissionGroup;
+
+public final class PermissionsSettingsAdapter extends SettingsAdapter<AppPermissionGroup> {
+ private Resources mRes;
+
+ public PermissionsSettingsAdapter(Context context) {
+ super(context, R.layout.permissions_settings_item);
+ mRes = context.getResources();
+ }
+
+ @Override
+ public WearableListView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return new PermissionsViewHolder(new PermissionsSettingsItem(parent.getContext()));
+ }
+
+ @Override
+ public void onBindViewHolder(WearableListView.ViewHolder holder, int position) {
+ super.onBindViewHolder(holder, position);
+ PermissionsViewHolder viewHolder = (PermissionsViewHolder) holder;
+ AppPermissionGroup group = get(position).data;
+
+ if (group.isPolicyFixed()) {
+ viewHolder.imageView.setEnabled(false);
+ viewHolder.textView.setEnabled(false);
+ viewHolder.state.setEnabled(false);
+ viewHolder.state.setText(
+ mRes.getString(R.string.permission_summary_enforced_by_policy));
+ } else {
+ viewHolder.imageView.setEnabled(true);
+ viewHolder.textView.setEnabled(true);
+ viewHolder.state.setEnabled(true);
+
+ if (group.areRuntimePermissionsGranted()) {
+ viewHolder.state.setText(R.string.generic_enabled);
+ } else {
+ viewHolder.state.setText(R.string.generic_disabled);
+ }
+ }
+ }
+
+ private static final class PermissionsViewHolder extends SettingsAdapter.SettingsItemHolder {
+ public final TextView state;
+
+ public PermissionsViewHolder(View view) {
+ super(view);
+ state = (TextView) view.findViewById(R.id.state);
+ }
+ }
+
+ private class PermissionsSettingsItem extends SettingsItem {
+ private final TextView mState;
+ private final float mCenteredAlpha = 1.0f;
+ private final float mNonCenteredAlpha = 0.5f;
+
+ public PermissionsSettingsItem (Context context) {
+ super(context);
+ mState = (TextView) findViewById(R.id.state);
+ }
+
+ @Override
+ public void onCenterPosition(boolean animate) {
+ mImage.setAlpha(mImage.isEnabled() ? mCenteredAlpha : mNonCenteredAlpha);
+ mText.setAlpha(mText.isEnabled() ? mCenteredAlpha : mNonCenteredAlpha);
+ mState.setAlpha(mState.isEnabled() ? mCenteredAlpha : mNonCenteredAlpha);
+ }
+
+ @Override
+ public void onNonCenterPosition(boolean animate) {
+ mImage.setAlpha(mNonCenteredAlpha);
+ mText.setAlpha(mNonCenteredAlpha);
+ mState.setAlpha(mNonCenteredAlpha);
+ }
+ }
+}
+
diff --git a/src/com/android/packageinstaller/permission/ui/wear/settings/SettingsAdapter.java b/src/com/android/packageinstaller/permission/ui/wear/settings/SettingsAdapter.java
new file mode 100644
index 00000000..baf1a2b4
--- /dev/null
+++ b/src/com/android/packageinstaller/permission/ui/wear/settings/SettingsAdapter.java
@@ -0,0 +1,276 @@
+/*
+ * 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.packageinstaller.permission.ui.wear.settings;
+
+import android.content.Context;
+import android.support.wearable.view.CircledImageView;
+import android.support.wearable.view.WearableListView;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.packageinstaller.R;
+
+import java.util.ArrayList;
+
+/**
+ * Common adapter for settings views. Maintains a list of 'Settings', consisting of a name,
+ * icon and optional activity-specific data.
+ */
+public class SettingsAdapter<T> extends WearableListView.Adapter {
+ private static final String TAG = "SettingsAdapter";
+ private final Context mContext;
+
+ public static final class Setting<S> {
+ public static final int ID_INVALID = -1;
+
+ public final int id;
+ public int nameResourceId;
+ public CharSequence name;
+ public int iconResource;
+ public boolean inProgress;
+ public S data;
+
+ public Setting(CharSequence name, int iconResource, S data) {
+ this(name, iconResource, data, ID_INVALID);
+ }
+
+ public Setting(CharSequence name, int iconResource, S data, int id) {
+ this.name = name;
+ this.iconResource = iconResource;
+ this.data = data;
+ this.inProgress = false;
+ this.id = id;
+ }
+
+ public Setting(int nameResource, int iconResource, S data, int id) {
+ this.nameResourceId = nameResource;
+ this.iconResource = iconResource;
+ this.data = data;
+ this.inProgress = false;
+ this.id = id;
+ }
+
+ public Setting(int nameResource, int iconResource, int id) {
+ this.nameResourceId = nameResource;
+ this.iconResource = iconResource;
+ this.data = null;
+ this.inProgress = false;
+ this.id = id;
+ }
+
+ public Setting(CharSequence name, int iconResource, int id) {
+ this(name, iconResource, null, id);
+ }
+
+ }
+
+ private final int mItemLayoutId;
+ private final float mDefaultCircleRadiusPercent;
+ private final float mSelectedCircleRadiusPercent;
+
+ protected ArrayList<Setting<T>> mSettings = new ArrayList<Setting<T>>();
+
+ public SettingsAdapter(Context context, int itemLayoutId) {
+ mContext = context;
+ mItemLayoutId = itemLayoutId;
+ mDefaultCircleRadiusPercent = context.getResources().getFraction(
+ R.dimen.default_settings_circle_radius_percent, 1, 1);
+ mSelectedCircleRadiusPercent = context.getResources().getFraction(
+ R.dimen.selected_settings_circle_radius_percent, 1, 1);
+ }
+
+ @Override
+ public WearableListView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return new SettingsItemHolder(new SettingsItem(parent.getContext()));
+ }
+
+ @Override
+ public void onBindViewHolder(WearableListView.ViewHolder holder, int position) {
+ Setting<T> setting = mSettings.get(position);
+ if (setting.iconResource == -1) {
+ ((SettingsItemHolder) holder).imageView.setVisibility(View.GONE);
+ } else {
+ ((SettingsItemHolder) holder).imageView.setVisibility(View.VISIBLE);
+ ((SettingsItemHolder) holder).imageView.setImageResource(
+ mSettings.get(position).iconResource);
+ }
+ Log.d(TAG, "onBindViewHolder " + setting.name + " " + setting.id + " " + setting
+ .nameResourceId);
+ if (setting.name == null && setting.nameResourceId != 0) {
+ setting.name = mContext.getString(setting.nameResourceId);
+ }
+ ((SettingsItemHolder) holder).textView.setText(setting.name);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mSettings.size();
+ }
+
+ public void addSetting(CharSequence name, int iconResource) {
+ addSetting(name, iconResource, null);
+ }
+
+ public void addSetting(CharSequence name, int iconResource, T intent) {
+ addSetting(mSettings.size(), name, iconResource, intent);
+ }
+
+ public void addSetting(int index, CharSequence name, int iconResource, T intent) {
+ addSetting(Setting.ID_INVALID, index, name, iconResource, intent);
+ }
+
+ public void addSetting(int id, int index, CharSequence name, int iconResource, T intent) {
+ mSettings.add(index, new Setting<T>(name, iconResource, intent, id));
+ notifyItemInserted(index);
+ }
+
+ public void addSettingDontNotify(Setting<T> setting) {
+ mSettings.add(setting);
+ }
+
+ public void addSetting(Setting<T> setting) {
+ mSettings.add(setting);
+ notifyItemInserted(mSettings.size() - 1);
+ }
+
+ public void addSetting(int index, Setting<T> setting) {
+ mSettings.add(index, setting);
+ notifyItemInserted(index);
+ }
+
+ /**
+ * Returns the index of the setting in the adapter based on the ID supplied when it was
+ * originally added.
+ * @param id the setting's id
+ * @return index in the adapter of the setting. -1 if not found.
+ */
+ public int findSetting(int id) {
+ for (int i = mSettings.size() - 1; i >= 0; --i) {
+ Setting setting = mSettings.get(i);
+
+ if (setting.id == id) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Removes a setting at the given index.
+ * @param index the index of the setting to be removed
+ */
+ public void removeSetting(int index) {
+ mSettings.remove(index);
+ notifyDataSetChanged();
+ }
+
+ public void clearSettings() {
+ mSettings.clear();
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Updates a setting in place.
+ * @param index the index of the setting
+ * @param name the updated setting name
+ * @param iconResource the update setting icon
+ * @param intent the updated intent for the setting
+ */
+ public void updateSetting(int index, CharSequence name, int iconResource, T intent) {
+ Setting<T> setting = mSettings.get(index);
+ setting.iconResource = iconResource;
+ setting.name = name;
+ setting.data = intent;
+ notifyItemChanged(index);
+ }
+
+ public Setting<T> get(int position) {
+ return mSettings.get(position);
+ }
+
+ protected static class SettingsItemHolder extends ExtendedViewHolder {
+ public final CircledImageView imageView;
+ public final TextView textView;
+
+ public SettingsItemHolder(View itemView) {
+ super(itemView);
+
+ imageView = ((CircledImageView) itemView.findViewById(R.id.image));
+ textView = ((TextView) itemView.findViewById(R.id.text));
+ }
+ }
+
+ protected class SettingsItem extends FrameLayout implements ExtendedOnCenterProximityListener {
+
+ protected final CircledImageView mImage;
+ protected final TextView mText;
+
+ public SettingsItem(Context context) {
+ super(context);
+ View view = View.inflate(context, mItemLayoutId, null);
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.MATCH_PARENT);
+ params.gravity = Gravity.CENTER_VERTICAL;
+ addView(view, params);
+ mImage = (CircledImageView) findViewById(R.id.image);
+ mText = (TextView) findViewById(R.id.text);
+ }
+
+ @Override
+ public float getProximityMinValue() {
+ return mDefaultCircleRadiusPercent;
+ }
+
+ @Override
+ public float getProximityMaxValue() {
+ return mSelectedCircleRadiusPercent;
+ }
+
+ @Override
+ public float getCurrentProximityValue() {
+ return mImage.getCircleRadiusPressedPercent();
+ }
+
+ @Override
+ public void setScalingAnimatorValue(float value) {
+ mImage.setCircleRadiusPercent(value);
+ mImage.setCircleRadiusPressedPercent(value);
+ }
+
+ @Override
+ public void onCenterPosition(boolean animate) {
+ mImage.setAlpha(1f);
+ mText.setAlpha(1f);
+ }
+
+ @Override
+ public void onNonCenterPosition(boolean animate) {
+ mImage.setAlpha(0.5f);
+ mText.setAlpha(0.5f);
+ }
+
+ TextView getTextView() {
+ return mText;
+ }
+ }
+}
diff --git a/src/com/android/packageinstaller/permission/ui/wear/settings/ViewUtils.java b/src/com/android/packageinstaller/permission/ui/wear/settings/ViewUtils.java
new file mode 100644
index 00000000..cf1c0fd0
--- /dev/null
+++ b/src/com/android/packageinstaller/permission/ui/wear/settings/ViewUtils.java
@@ -0,0 +1,48 @@
+/*
+ * 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.packageinstaller.permission.ui.wear.settings;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Utility to determine screen shape
+ */
+public class ViewUtils {
+
+ public static boolean getIsCircular(Context context) {
+ return context.getResources().getConfiguration().isScreenRound();
+ }
+
+ /**
+ * Set the given {@code view} and all descendants to the given {@code enabled} state.
+ *
+ * @param view the parent view of a subtree of components whose enabled state must be set
+ * @param enabled the new enabled state of the subtree of components
+ */
+ public static void setEnabled(View view, boolean enabled) {
+ view.setEnabled(enabled);
+
+ if (view instanceof ViewGroup) {
+ final ViewGroup viewGroup = (ViewGroup) view;
+ for (int i = 0; i < viewGroup.getChildCount(); i++) {
+ setEnabled(viewGroup.getChildAt(i), enabled);
+ }
+ }
+ }
+}