diff options
Diffstat (limited to 'src/com/android/packageinstaller/permission/ui')
30 files changed, 3573 insertions, 33 deletions
diff --git a/src/com/android/packageinstaller/permission/ui/ButtonBarLayout.java b/src/com/android/packageinstaller/permission/ui/ButtonBarLayout.java new file mode 100644 index 00000000..59e54707 --- /dev/null +++ b/src/com/android/packageinstaller/permission/ui/ButtonBarLayout.java @@ -0,0 +1,117 @@ +/* + * 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; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.widget.LinearLayout; +import com.android.packageinstaller.R; + +/** + * An extension of LinearLayout that automatically switches to vertical + * orientation when it can't fit its child views horizontally. + */ +public class ButtonBarLayout extends LinearLayout { + /** Whether the current configuration allows stacking. */ + private boolean mAllowStacking; + + private int mLastWidthSize = -1; + + public ButtonBarLayout(Context context, AttributeSet attrs) { + super(context, attrs); + mAllowStacking = true; + } + + public void setAllowStacking(boolean allowStacking) { + if (mAllowStacking != allowStacking) { + mAllowStacking = allowStacking; + if (!mAllowStacking && getOrientation() == LinearLayout.VERTICAL) { + setStacked(false); + } + requestLayout(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int widthSize = MeasureSpec.getSize(widthMeasureSpec); + + if (mAllowStacking) { + if (widthSize > mLastWidthSize && isStacked()) { + // We're being measured wider this time, try un-stacking. + setStacked(false); + } + + mLastWidthSize = widthSize; + } + + boolean needsRemeasure = false; + + // If we're not stacked, make sure the measure spec is AT_MOST rather + // than EXACTLY. This ensures that we'll still get TOO_SMALL so that we + // know to stack the buttons. + final int initialWidthMeasureSpec; + if (!isStacked() && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) { + initialWidthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST); + + // We'll need to remeasure again to fill excess space. + needsRemeasure = true; + } else { + initialWidthMeasureSpec = widthMeasureSpec; + } + + super.onMeasure(initialWidthMeasureSpec, heightMeasureSpec); + + if (mAllowStacking && !isStacked()) { + final int measuredWidth = getMeasuredWidthAndState(); + final int measuredWidthState = measuredWidth & MEASURED_STATE_MASK; + if (measuredWidthState == MEASURED_STATE_TOO_SMALL) { + setStacked(true); + + // Measure again in the new orientation. + needsRemeasure = true; + } + } + + if (needsRemeasure) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + + private void setStacked(boolean stacked) { + setOrientation(stacked ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL); + setGravity(stacked ? Gravity.RIGHT : Gravity.BOTTOM); + + final View spacer = findViewById(R.id.spacer); + if (spacer != null) { + spacer.setVisibility(stacked ? View.GONE : View.INVISIBLE); + } + + // Reverse the child order. This is specific to the Material button + // bar's layout XML and will probably not generalize. + final int childCount = getChildCount(); + for (int i = childCount - 2; i >= 0; i--) { + bringChildToFront(getChildAt(i)); + } + } + + private boolean isStacked() { + return getOrientation() == LinearLayout.VERTICAL; + } +} diff --git a/src/com/android/packageinstaller/permission/ui/GrantPermissionsActivity.java b/src/com/android/packageinstaller/permission/ui/GrantPermissionsActivity.java index 56b3f466..102fd6ef 100644 --- a/src/com/android/packageinstaller/permission/ui/GrantPermissionsActivity.java +++ b/src/com/android/packageinstaller/permission/ui/GrantPermissionsActivity.java @@ -26,11 +26,12 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PermissionInfo; import android.content.res.Resources; +import android.graphics.Typeface; import android.graphics.drawable.Icon; import android.hardware.camera2.utils.ArrayUtils; import android.os.Bundle; import android.text.SpannableString; -import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; @@ -38,6 +39,7 @@ import android.view.View; import android.view.Window; import android.view.WindowManager; +import com.android.packageinstaller.DeviceUtils; import com.android.packageinstaller.R; import com.android.packageinstaller.permission.model.AppPermissionGroup; import com.android.packageinstaller.permission.model.AppPermissions; @@ -71,10 +73,14 @@ public class GrantPermissionsActivity extends OverlayTouchActivity setTitle(R.string.permission_request_title); - if (Utils.isTelevision(this)) { - mViewHandler = new GrantPermissionsTvViewHandler(this).setResultListener(this); + if (DeviceUtils.isTelevision(this)) { + mViewHandler = new com.android.packageinstaller.permission.ui.television + .GrantPermissionsViewHandlerImpl(this).setResultListener(this); + } else if (DeviceUtils.isWear(this)) { + mViewHandler = new GrantPermissionsWatchViewHandler(this).setResultListener(this); } else { - mViewHandler = new GrantPermissionsDefaultViewHandler(this).setResultListener(this); + mViewHandler = new com.android.packageinstaller.permission.ui.handheld + .GrantPermissionsViewHandlerImpl(this).setResultListener(this); } mRequestedPermissions = getIntent().getStringArrayExtra( @@ -206,8 +212,7 @@ public class GrantPermissionsActivity extends OverlayTouchActivity // Color the app name. int appLabelStart = message.toString().indexOf(appLabel.toString(), 0); int appLabelLength = appLabel.length(); - int color = getColor(R.color.grant_permissions_app_color); - message.setSpan(new ForegroundColorSpan(color), appLabelStart, + message.setSpan(new StyleSpan(Typeface.BOLD), appLabelStart, appLabelStart + appLabelLength, 0); // Set the new grant view diff --git a/src/com/android/packageinstaller/permission/ui/GrantPermissionsViewHandler.java b/src/com/android/packageinstaller/permission/ui/GrantPermissionsViewHandler.java index 4032abb2..5e2259af 100644 --- a/src/com/android/packageinstaller/permission/ui/GrantPermissionsViewHandler.java +++ b/src/com/android/packageinstaller/permission/ui/GrantPermissionsViewHandler.java @@ -25,7 +25,7 @@ import android.view.WindowManager; * Class for managing the presentation and user interaction of the "grant * permissions" user interface. */ -interface GrantPermissionsViewHandler { +public interface GrantPermissionsViewHandler { /** * Listener interface for getting notified when the user responds to a diff --git a/src/com/android/packageinstaller/permission/ui/GrantPermissionsWatchViewHandler.java b/src/com/android/packageinstaller/permission/ui/GrantPermissionsWatchViewHandler.java new file mode 100644 index 00000000..21042f00 --- /dev/null +++ b/src/com/android/packageinstaller/permission/ui/GrantPermissionsWatchViewHandler.java @@ -0,0 +1,176 @@ +package com.android.packageinstaller.permission.ui; + +import android.content.Context; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.WindowManager; + +import com.android.packageinstaller.R; +import com.android.packageinstaller.permission.ui.wear.ConfirmationViewHandler; + +/** + * Watch-specific view handler for the grant permissions activity. + */ +final class GrantPermissionsWatchViewHandler extends ConfirmationViewHandler + implements GrantPermissionsViewHandler { + private static final String TAG = "GrantPermsWatchViewH"; + + private static final String ARG_GROUP_NAME = "ARG_GROUP_NAME"; + + private final Context mContext; + + private ResultListener mResultListener; + + private String mGroupName; + private boolean mShowDoNotAsk; + + private CharSequence mMessage; + private String mCurrentPageText; + private Icon mIcon; + + GrantPermissionsWatchViewHandler(Context context) { + super(context); + mContext = context; + } + + @Override + public GrantPermissionsWatchViewHandler setResultListener(ResultListener listener) { + mResultListener = listener; + return this; + } + + @Override + public View createView() { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "createView()"); + } + + mShowDoNotAsk = false; + + return super.createView(); + } + + @Override + public void updateWindowAttributes(WindowManager.LayoutParams outLayoutParams) { + outLayoutParams.width = WindowManager.LayoutParams.MATCH_PARENT; + outLayoutParams.height = WindowManager.LayoutParams.MATCH_PARENT; + outLayoutParams.format = PixelFormat.OPAQUE; + outLayoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG; + outLayoutParams.flags |= WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + } + + @Override + public void updateUi(String groupName, int groupCount, int groupIndex, Icon icon, + CharSequence message, boolean showDoNotAsk) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "updateUi() - groupName: " + groupName + + ", groupCount: " + groupCount + + ", groupIndex: " + groupIndex + + ", icon: " + icon + + ", message: " + message + + ", showDoNotAsk: " + showDoNotAsk); + } + + mGroupName = groupName; + mShowDoNotAsk = showDoNotAsk; + mMessage = message; + mIcon = icon; + mCurrentPageText = (groupCount > 1 ? + mContext.getString(R.string.current_permission_template, groupIndex + 1, groupCount) + : null); + + invalidate(); + } + + @Override + public void saveInstanceState(Bundle outState) { + outState.putString(ARG_GROUP_NAME, mGroupName); + } + + @Override + public void loadInstanceState(Bundle savedInstanceState) { + mGroupName = savedInstanceState.getString(ARG_GROUP_NAME); + } + + @Override + public void onBackPressed() { + if (mResultListener != null) { + mResultListener.onPermissionGrantResult(mGroupName, false, false); + } + } + + @Override // ConfirmationViewHandler + public void onButton1() { + onClick(true /* granted */, false /* doNotAskAgain */); + } + + @Override // ConfirmationViewHandler + public void onButton2() { + onClick(false /* granted */, false /* doNotAskAgain */); + } + + @Override // ConfirmationViewHandler + public void onButton3() { + onClick(false /* granted */, true /* doNotAskAgain */); + } + + @Override // ConfirmationViewHandler + public CharSequence getCurrentPageText() { + return mCurrentPageText; + } + + @Override // ConfirmationViewHandler + public Icon getPermissionIcon() { + return mIcon; + } + + @Override // ConfirmationViewHandler + public CharSequence getMessage() { + return mMessage; + } + + @Override // ConfirmationViewHandler + public int getButtonBarMode() { + return mShowDoNotAsk ? MODE_VERTICAL_BUTTONS : MODE_HORIZONTAL_BUTTONS; + } + + @Override // ConfirmationViewHandler + public CharSequence getVerticalButton1Text() { + return mContext.getString(R.string.grant_dialog_button_allow); + } + + @Override // ConfirmationViewHandler + public CharSequence getVerticalButton2Text() { + return mContext.getString(R.string.grant_dialog_button_deny); + } + + @Override // ConfirmationViewHandler + public CharSequence getVerticalButton3Text() { + return mContext.getString(R.string.grant_dialog_button_deny_dont_ask_again); + } + + @Override // ConfirmationViewHandler + public Drawable getVerticalButton1Icon(){ + return mContext.getDrawable(R.drawable.confirm_button); + } + + @Override // ConfirmationViewHandler + public Drawable getVerticalButton2Icon(){ + return mContext.getDrawable(R.drawable.cancel_button); + } + + @Override // ConfirmationViewHandler + public Drawable getVerticalButton3Icon(){ + return mContext.getDrawable(R.drawable.deny_button); + } + + private void onClick(boolean granted, boolean doNotAskAgain) { + if (mResultListener != null) { + mResultListener.onPermissionGrantResult(mGroupName, granted, doNotAskAgain); + } + } +} diff --git a/src/com/android/packageinstaller/permission/ui/ManagePermissionsActivity.java b/src/com/android/packageinstaller/permission/ui/ManagePermissionsActivity.java index 8ba6b127..38dbf8f5 100644 --- a/src/com/android/packageinstaller/permission/ui/ManagePermissionsActivity.java +++ b/src/com/android/packageinstaller/permission/ui/ManagePermissionsActivity.java @@ -21,6 +21,9 @@ import android.content.Intent; import android.os.Bundle; import android.util.Log; +import com.android.packageinstaller.permission.ui.wear.AppPermissionsFragmentWear; +import com.android.packageinstaller.DeviceUtils; + public final class ManagePermissionsActivity extends OverlayTouchActivity { private static final String LOG_TAG = "ManagePermissionsActivity"; @@ -37,7 +40,13 @@ public final class ManagePermissionsActivity extends OverlayTouchActivity { switch (action) { case Intent.ACTION_MANAGE_PERMISSIONS: { - fragment = ManagePermissionsFragment.newInstance(); + if (DeviceUtils.isTelevision(this)) { + fragment = com.android.packageinstaller.permission.ui.television + .ManagePermissionsFragment.newInstance(); + } else { + fragment = com.android.packageinstaller.permission.ui.handheld + .ManagePermissionsFragment.newInstance(); + } } break; case Intent.ACTION_MANAGE_APP_PERMISSIONS: { @@ -47,7 +56,15 @@ public final class ManagePermissionsActivity extends OverlayTouchActivity { finish(); return; } - fragment = AppPermissionsFragment.newInstance(packageName); + if (DeviceUtils.isWear(this)) { + fragment = AppPermissionsFragmentWear.newInstance(packageName); + } else if (DeviceUtils.isTelevision(this)) { + fragment = com.android.packageinstaller.permission.ui.television + .AppPermissionsFragment.newInstance(packageName); + } else { + fragment = com.android.packageinstaller.permission.ui.handheld + .AppPermissionsFragment.newInstance(packageName); + } } break; case Intent.ACTION_MANAGE_PERMISSION_APPS: { @@ -57,7 +74,13 @@ public final class ManagePermissionsActivity extends OverlayTouchActivity { finish(); return; } - fragment = PermissionAppsFragment.newInstance(permissionName); + if (DeviceUtils.isTelevision(this)) { + fragment = com.android.packageinstaller.permission.ui.television + .PermissionAppsFragment.newInstance(permissionName); + } else { + fragment = com.android.packageinstaller.permission.ui.handheld + .PermissionAppsFragment.newInstance(permissionName); + } } break; default: { diff --git a/src/com/android/packageinstaller/permission/ui/OverlayWarningDialog.java b/src/com/android/packageinstaller/permission/ui/OverlayWarningDialog.java index a7c1e2a1..61734b47 100644 --- a/src/com/android/packageinstaller/permission/ui/OverlayWarningDialog.java +++ b/src/com/android/packageinstaller/permission/ui/OverlayWarningDialog.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.android.packageinstaller.permission.ui; import android.app.Activity; diff --git a/src/com/android/packageinstaller/permission/ui/PreferenceImageView.java b/src/com/android/packageinstaller/permission/ui/PreferenceImageView.java new file mode 100644 index 00000000..c3f51674 --- /dev/null +++ b/src/com/android/packageinstaller/permission/ui/PreferenceImageView.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2014 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; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; + +/** + * Extension of ImageView that correctly applies maxWidth and maxHeight. + */ +public class PreferenceImageView extends ImageView { + + public PreferenceImageView(Context context) { + this(context, null); + } + + public PreferenceImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PreferenceImageView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public PreferenceImageView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + if (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) { + final int widthSize = MeasureSpec.getSize(widthMeasureSpec); + final int maxWidth = getMaxWidth(); + if (maxWidth != Integer.MAX_VALUE + && (maxWidth < widthSize || widthMode == MeasureSpec.UNSPECIFIED)) { + widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST); + } + } + + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) { + final int heightSize = MeasureSpec.getSize(heightMeasureSpec); + final int maxHeight = getMaxHeight(); + if (maxHeight != Integer.MAX_VALUE + && (maxHeight < heightSize || heightMode == MeasureSpec.UNSPECIFIED)) { + heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST); + } + } + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +} diff --git a/src/com/android/packageinstaller/permission/ui/handheld/AllAppPermissionsFragment.java b/src/com/android/packageinstaller/permission/ui/handheld/AllAppPermissionsFragment.java new file mode 100644 index 00000000..b3b0895c --- /dev/null +++ b/src/com/android/packageinstaller/permission/ui/handheld/AllAppPermissionsFragment.java @@ -0,0 +1,214 @@ +/* +* 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.handheld; + +import android.app.ActionBar; +import android.app.AlertDialog; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageItemInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.PermissionGroupInfo; +import android.content.pm.PermissionInfo; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceCategory; +import android.preference.PreferenceGroup; +import android.provider.Settings; +import android.util.Log; +import android.view.MenuItem; +import com.android.packageinstaller.R; +import com.android.packageinstaller.permission.utils.Utils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +public final class AllAppPermissionsFragment extends SettingsWithHeader { + + private static final String LOG_TAG = "AllAppPermissionsFragment"; + + private static final String KEY_OTHER = "other_perms"; + + public static AllAppPermissionsFragment newInstance(String packageName) { + AllAppPermissionsFragment instance = new AllAppPermissionsFragment(); + Bundle arguments = new Bundle(); + arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName); + instance.setArguments(arguments); + return instance; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + final ActionBar ab = getActivity().getActionBar(); + if (ab != null) { + ab.setTitle(R.string.all_permissions); + ab.setDisplayHomeAsUpEnabled(true); + } + } + + @Override + public void onResume() { + super.onResume(); + updateUi(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: { + getFragmentManager().popBackStack(); + return true; + } + } + return super.onOptionsItemSelected(item); + } + + private void updateUi() { + if (getPreferenceScreen() != null) { + getPreferenceScreen().removeAll(); + } + addPreferencesFromResource(R.xml.all_permissions); + PreferenceGroup otherGroup = (PreferenceGroup) findPreference(KEY_OTHER); + ArrayList<Preference> prefs = new ArrayList<>(); // Used for sorting. + prefs.add(otherGroup); + String pkg = getArguments().getString(Intent.EXTRA_PACKAGE_NAME); + otherGroup.removeAll(); + PackageManager pm = getContext().getPackageManager(); + + try { + PackageInfo info = pm.getPackageInfo(pkg, PackageManager.GET_PERMISSIONS); + + ApplicationInfo appInfo = info.applicationInfo; + final Drawable icon = appInfo.loadIcon(pm); + final CharSequence label = appInfo.loadLabel(pm); + Intent infoIntent = null; + if (!getActivity().getIntent().getBooleanExtra( + AppPermissionsFragment.EXTRA_HIDE_INFO_BUTTON, false)) { + infoIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData(Uri.fromParts("package", pkg, null)); + } + setHeader(icon, label, infoIntent); + + if (info.requestedPermissions != null) { + for (int i = 0; i < info.requestedPermissions.length; i++) { + PermissionInfo perm; + try { + perm = pm.getPermissionInfo(info.requestedPermissions[i], 0); + } catch (NameNotFoundException e) { + Log.e(LOG_TAG, + "Can't get permission info for " + info.requestedPermissions[i], e); + continue; + } + + if ((perm.flags & PermissionInfo.FLAG_INSTALLED) == 0 + || (perm.flags & PermissionInfo.FLAG_HIDDEN) != 0) { + continue; + } + + if (perm.protectionLevel == PermissionInfo.PROTECTION_DANGEROUS) { + PermissionGroupInfo group = getGroup(perm.group, pm); + PreferenceGroup pref = + findOrCreate(group != null ? group : perm, pm, prefs); + pref.addPreference(getPreference(perm, group, pm)); + } else if (perm.protectionLevel == PermissionInfo.PROTECTION_NORMAL) { + PermissionGroupInfo group = getGroup(perm.group, pm); + otherGroup.addPreference(getPreference(perm, group, pm)); + } + } + } + } catch (NameNotFoundException e) { + Log.e(LOG_TAG, "Problem getting package info for " + pkg, e); + } + // Sort an ArrayList of the groups and then set the order from the sorting. + Collections.sort(prefs, new Comparator<Preference>() { + @Override + public int compare(Preference lhs, Preference rhs) { + String lKey = lhs.getKey(); + String rKey = rhs.getKey(); + if (lKey.equals(KEY_OTHER)) { + return 1; + } else if (rKey.equals(KEY_OTHER)) { + return -1; + } else if (Utils.isModernPermissionGroup(lKey) + != Utils.isModernPermissionGroup(rKey)) { + return Utils.isModernPermissionGroup(lKey) ? -1 : 1; + } + return lhs.getTitle().toString().compareTo(rhs.getTitle().toString()); + } + }); + for (int i = 0; i < prefs.size(); i++) { + prefs.get(i).setOrder(i); + } + } + + private PermissionGroupInfo getGroup(String group, PackageManager pm) { + try { + return pm.getPermissionGroupInfo(group, 0); + } catch (NameNotFoundException e) { + return null; + } + } + + private PreferenceGroup findOrCreate(PackageItemInfo group, PackageManager pm, + ArrayList<Preference> prefs) { + PreferenceGroup pref = (PreferenceGroup) findPreference(group.name); + if (pref == null) { + pref = new PreferenceCategory(getContext()); + pref.setKey(group.name); + pref.setTitle(group.loadLabel(pm)); + prefs.add(pref); + getPreferenceScreen().addPreference(pref); + } + return pref; + } + + private Preference getPreference(PermissionInfo perm, PermissionGroupInfo group, + PackageManager pm) { + Preference pref = new Preference(getContext()); + Drawable icon = null; + if (perm.icon != 0) { + icon = perm.loadIcon(pm); + } else if (group != null && group.icon != 0) { + icon = group.loadIcon(pm); + } else { + icon = getContext().getDrawable(R.drawable.ic_perm_device_info); + } + pref.setIcon(Utils.applyTint(getContext(), icon, android.R.attr.colorControlNormal)); + pref.setTitle(perm.loadLabel(pm)); + final CharSequence desc = perm.loadDescription(pm); + pref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + new AlertDialog.Builder(getContext()) + .setMessage(desc) + .setPositiveButton(android.R.string.ok, null) + .show(); + return true; + } + }); + + return pref; + } +}
\ No newline at end of file diff --git a/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionsFragment.java b/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionsFragment.java new file mode 100644 index 00000000..f56cba70 --- /dev/null +++ b/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionsFragment.java @@ -0,0 +1,404 @@ +/* +* 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.handheld; + +import android.annotation.Nullable; +import android.app.ActionBar; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Fragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.provider.Settings; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +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.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 AppPermissionsFragment extends SettingsWithHeader + implements OnPreferenceChangeListener { + + private static final String LOG_TAG = "ManagePermsFragment"; + + static final String EXTRA_HIDE_INFO_BUTTON = "hideInfoButton"; + + private static final int MENU_ALL_PERMS = 0; + + private List<AppPermissionGroup> mToggledGroups; + private AppPermissions mAppPermissions; + private PreferenceScreen mExtraScreen; + + private boolean mHasConfirmedRevoke; + + public static AppPermissionsFragment newInstance(String packageName) { + return setPackageName(new AppPermissionsFragment(), 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); + setLoading(true /* loading */, false /* animate */); + setHasOptionsMenu(true); + final ActionBar ab = getActivity().getActionBar(); + if (ab != null) { + ab.setDisplayHomeAsUpEnabled(true); + } + + String packageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME); + Activity activity = getActivity(); + PackageInfo packageInfo = getPackageInfo(activity, packageName); + 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(); + } + }); + loadPreferences(); + } + + @Override + public void onResume() { + super.onResume(); + mAppPermissions.refresh(); + setPreferencesCheckedState(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: { + getActivity().finish(); + return true; + } + + case MENU_ALL_PERMS: { + Fragment frag = AllAppPermissionsFragment.newInstance( + getArguments().getString(Intent.EXTRA_PACKAGE_NAME)); + getFragmentManager().beginTransaction() + .replace(android.R.id.content, frag) + .addToBackStack("AllPerms") + .commit(); + return true; + } + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + if (mAppPermissions != null) { + bindUi(this, mAppPermissions.getPackageInfo()); + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + menu.add(Menu.NONE, MENU_ALL_PERMS, Menu.NONE, R.string.all_permissions); + } + + private static void bindUi(SettingsWithHeader fragment, PackageInfo packageInfo) { + Activity activity = fragment.getActivity(); + PackageManager pm = activity.getPackageManager(); + ApplicationInfo appInfo = packageInfo.applicationInfo; + Intent infoIntent = null; + if (!activity.getIntent().getBooleanExtra(EXTRA_HIDE_INFO_BUTTON, false)) { + infoIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData(Uri.fromParts("package", packageInfo.packageName, null)); + } + + Drawable icon = appInfo.loadIcon(pm); + CharSequence label = appInfo.loadLabel(pm); + fragment.setHeader(icon, label, infoIntent); + + ActionBar ab = activity.getActionBar(); + if (ab != null) { + ab.setTitle(R.string.app_permissions); + } + + ViewGroup rootView = (ViewGroup) fragment.getView(); + ImageView iconView = (ImageView) rootView.findViewById(R.id.lb_icon); + if (iconView != null) { + iconView.setImageDrawable(icon); + } + TextView titleView = (TextView) rootView.findViewById(R.id.lb_title); + if (titleView != null) { + titleView.setText(R.string.app_permissions); + } + TextView breadcrumbView = (TextView) rootView.findViewById(R.id.lb_breadcrumb); + if (breadcrumbView != null) { + breadcrumbView.setText(label); + } + } + + private void loadPreferences() { + Context context = getActivity(); + if (context == null) { + return; + } + + PreferenceScreen screen = getPreferenceScreen(); + if (screen == null) { + screen = getPreferenceManager().createPreferenceScreen(getActivity()); + setPreferenceScreen(screen); + } + + screen.removeAll(); + + if (mExtraScreen != null) { + mExtraScreen.removeAll(); + } + + final Preference extraPerms = new Preference(context); + extraPerms.setIcon(R.drawable.ic_toc); + extraPerms.setTitle(R.string.additional_permissions); + + for (AppPermissionGroup group : mAppPermissions.getPermissionGroups()) { + if (!Utils.shouldShowPermission(group, mAppPermissions.getPackageInfo().packageName)) { + continue; + } + + boolean isPlatform = group.getDeclaringPackage().equals(Utils.OS_PKG); + + SwitchPreference preference = new SwitchPreference(context); + preference.setOnPreferenceChangeListener(this); + preference.setKey(group.getName()); + Drawable icon = Utils.loadDrawable(context.getPackageManager(), + group.getIconPkg(), group.getIconResId()); + preference.setIcon(Utils.applyTint(getContext(), icon, + android.R.attr.colorControlNormal)); + preference.setTitle(group.getLabel()); + if (group.isPolicyFixed()) { + preference.setSummary(getString(R.string.permission_summary_enforced_by_policy)); + } + preference.setPersistent(false); + preference.setEnabled(!group.isPolicyFixed()); + preference.setChecked(group.areRuntimePermissionsGranted()); + + if (isPlatform) { + screen.addPreference(preference); + } else { + if (mExtraScreen == null) { + mExtraScreen = getPreferenceManager().createPreferenceScreen(context); + } + mExtraScreen.addPreference(preference); + } + } + + if (mExtraScreen != null) { + extraPerms.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + AdditionalPermissionsFragment frag = new AdditionalPermissionsFragment(); + setPackageName(frag, getArguments().getString(Intent.EXTRA_PACKAGE_NAME)); + frag.setTargetFragment(AppPermissionsFragment.this, 0); + getFragmentManager().beginTransaction() + .replace(android.R.id.content, frag) + .addToBackStack(null) + .commit(); + return true; + } + }); + int count = mExtraScreen.getPreferenceCount(); + extraPerms.setSummary(getResources().getQuantityString( + R.plurals.additional_permissions_more, count, count)); + screen.addPreference(extraPerms); + } + + setLoading(false /* loading */, true /* animate */); + } + + @Override + public boolean onPreferenceChange(final Preference preference, Object newValue) { + String groupName = preference.getKey(); + final AppPermissionGroup group = mAppPermissions.getPermissionGroup(groupName); + + if (group == null) { + return false; + } + + OverlayTouchActivity activity = (OverlayTouchActivity) getActivity(); + if (activity.isObscuredTouch()) { + activity.showOverlayDialog(); + return false; + } + + addToggledGroup(group); + + if (LocationUtils.isLocationGroupAndProvider(group.getName(), group.getApp().packageName)) { + LocationUtils.showLocationDialog(getContext(), mAppPermissions.getAppLabel()); + return false; + } + if (newValue == Boolean.TRUE) { + group.grantRuntimePermissions(false); + } else { + final boolean grantedByDefault = group.hasGrantedByDefaultPermission(); + if (grantedByDefault || (!group.hasRuntimePermission() && !mHasConfirmedRevoke)) { + new AlertDialog.Builder(getContext()) + .setMessage(grantedByDefault ? R.string.system_warning + : R.string.old_sdk_deny_warning) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.grant_dialog_button_deny, + new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + ((SwitchPreference) preference).setChecked(false); + group.revokeRuntimePermissions(false); + if (!grantedByDefault) { + mHasConfirmedRevoke = true; + } + } + }) + .show(); + return false; + } else { + group.revokeRuntimePermissions(false); + } + } + + return true; + } + + @Override + public void onPause() { + super.onPause(); + logToggledGroups(); + } + + 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 logToggledGroups() { + if (mToggledGroups != null) { + String packageName = mAppPermissions.getPackageInfo().packageName; + SafetyNetLogger.logPermissionsToggled(packageName, mToggledGroups); + mToggledGroups = null; + } + } + + private void setPreferencesCheckedState() { + setPreferencesCheckedState(getPreferenceScreen()); + if (mExtraScreen != null) { + setPreferencesCheckedState(mExtraScreen); + } + } + + private void setPreferencesCheckedState(PreferenceScreen screen) { + int preferenceCount = screen.getPreferenceCount(); + for (int i = 0; i < preferenceCount; i++) { + Preference preference = screen.getPreference(i); + if (preference instanceof SwitchPreference) { + SwitchPreference switchPref = (SwitchPreference) preference; + AppPermissionGroup group = mAppPermissions.getPermissionGroup(switchPref.getKey()); + if (group != null) { + switchPref.setChecked(group.areRuntimePermissionsGranted()); + } + } + } + } + + private static PackageInfo getPackageInfo(Activity activity, String packageName) { + try { + return activity.getPackageManager().getPackageInfo( + packageName, PackageManager.GET_PERMISSIONS); + } catch (PackageManager.NameNotFoundException e) { + Log.i(LOG_TAG, "No package:" + activity.getCallingPackage(), e); + return null; + } + } + + public static class AdditionalPermissionsFragment extends SettingsWithHeader { + AppPermissionsFragment mOuterFragment; + + @Override + public void onCreate(Bundle savedInstanceState) { + mOuterFragment = (AppPermissionsFragment) getTargetFragment(); + super.onCreate(savedInstanceState); + setHeader(mOuterFragment.mIcon, mOuterFragment.mLabel, mOuterFragment.mInfoIntent); + setHasOptionsMenu(true); + setPreferenceScreen(mOuterFragment.mExtraScreen); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + String packageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME); + bindUi(this, getPackageInfo(getActivity(), packageName)); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + getFragmentManager().popBackStack(); + return true; + } + return super.onOptionsItemSelected(item); + } + } +} diff --git a/src/com/android/packageinstaller/permission/ui/GrantPermissionsDefaultViewHandler.java b/src/com/android/packageinstaller/permission/ui/handheld/GrantPermissionsViewHandlerImpl.java index c5d78784..2d27f069 100644 --- a/src/com/android/packageinstaller/permission/ui/GrantPermissionsDefaultViewHandler.java +++ b/src/com/android/packageinstaller/permission/ui/handheld/GrantPermissionsViewHandlerImpl.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.packageinstaller.permission.ui; +package com.android.packageinstaller.permission.ui.handheld; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -40,12 +40,14 @@ import android.widget.CheckBox; import android.widget.ImageView; import android.widget.TextView; -import com.android.internal.widget.ButtonBarLayout; import com.android.packageinstaller.R; +import com.android.packageinstaller.permission.ui.ButtonBarLayout; +import com.android.packageinstaller.permission.ui.GrantPermissionsViewHandler; +import com.android.packageinstaller.permission.ui.ManualLayoutFrame; import java.util.ArrayList; -final class GrantPermissionsDefaultViewHandler +public final class GrantPermissionsViewHandlerImpl implements GrantPermissionsViewHandler, OnClickListener { public static final String ARG_GROUP_NAME = "ARG_GROUP_NAME"; @@ -101,12 +103,12 @@ final class GrantPermissionsDefaultViewHandler } }; - GrantPermissionsDefaultViewHandler(Context context) { + public GrantPermissionsViewHandlerImpl(Context context) { mContext = context; } @Override - public GrantPermissionsDefaultViewHandler setResultListener(ResultListener listener) { + public GrantPermissionsViewHandlerImpl setResultListener(ResultListener listener) { mResultListener = listener; return this; } @@ -314,9 +316,7 @@ final class GrantPermissionsDefaultViewHandler public View createView() { mRootView = (ManualLayoutFrame) LayoutInflater.from(mContext) .inflate(R.layout.grant_permissions, null); - ((ButtonBarLayout) mRootView.findViewById(R.id.button_group)).setAllowStacking( - Resources.getSystem().getBoolean( - com.android.internal.R.bool.allow_stacked_button_bar)); + ((ButtonBarLayout) mRootView.findViewById(R.id.button_group)).setAllowStacking(true); mDialogContainer = (ViewGroup) mRootView.findViewById(R.id.dialog_container); mMessageView = (TextView) mRootView.findViewById(R.id.permission_message); diff --git a/src/com/android/packageinstaller/permission/ui/handheld/ManagePermissionsFragment.java b/src/com/android/packageinstaller/permission/ui/handheld/ManagePermissionsFragment.java new file mode 100644 index 00000000..c53da879 --- /dev/null +++ b/src/com/android/packageinstaller/permission/ui/handheld/ManagePermissionsFragment.java @@ -0,0 +1,268 @@ +/* + * 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.handheld; + +import android.annotation.Nullable; +import android.app.ActionBar; +import android.app.FragmentTransaction; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceScreen; +import android.util.ArraySet; +import android.util.Log; +import android.view.MenuItem; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; +import com.android.packageinstaller.R; +import com.android.packageinstaller.permission.model.PermissionApps; +import com.android.packageinstaller.permission.model.PermissionApps.PmCache; +import com.android.packageinstaller.permission.model.PermissionGroup; +import com.android.packageinstaller.permission.model.PermissionGroups; +import com.android.packageinstaller.permission.utils.Utils; + +import java.util.List; + +public final class ManagePermissionsFragment extends PermissionsFrameFragment + implements PermissionGroups.PermissionsGroupsChangeCallback, + Preference.OnPreferenceClickListener { + private static final String LOG_TAG = "ManagePermissionsFragment"; + + private static final String OS_PKG = "android"; + + private static final String EXTRA_PREFS_KEY = "extra_prefs_key"; + + private ArraySet<String> mLauncherPkgs; + + private PermissionGroups mPermissions; + + private PreferenceScreen mExtraScreen; + + public static ManagePermissionsFragment newInstance() { + return new ManagePermissionsFragment(); + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + setLoading(true /* loading */, false /* animate */); + setHasOptionsMenu(true); + final ActionBar ab = getActivity().getActionBar(); + if (ab != null) { + ab.setDisplayHomeAsUpEnabled(true); + } + mLauncherPkgs = Utils.getLauncherPackages(getContext()); + mPermissions = new PermissionGroups(getActivity(), getLoaderManager(), this); + } + + @Override + public void onResume() { + super.onResume(); + mPermissions.refresh(); + updatePermissionsUi(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + getActivity().finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + String key = preference.getKey(); + + PermissionGroup group = mPermissions.getGroup(key); + if (group == null) { + return false; + } + + Intent intent = new Intent(Intent.ACTION_MANAGE_PERMISSION_APPS) + .putExtra(Intent.EXTRA_PERMISSION_NAME, key); + try { + getActivity().startActivity(intent); + } catch (ActivityNotFoundException e) { + Log.w(LOG_TAG, "No app to handle " + intent); + } + + return true; + } + + @Override + public void onPermissionGroupsChanged() { + updatePermissionsUi(); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + bindPermissionUi(getActivity(), getView()); + } + + private static void bindPermissionUi(@Nullable Context context, @Nullable View rootView) { + if (context == null || rootView == null) { + return; + } + + ImageView iconView = (ImageView) rootView.findViewById(R.id.lb_icon); + if (iconView != null) { + // Set the icon as the background instead of the image because ImageView + // doesn't properly scale vector drawables beyond their intrinsic size + Drawable icon = context.getDrawable(R.drawable.ic_lock); + icon.setTint(context.getColor(R.color.off_white)); + iconView.setBackground(icon); + } + TextView titleView = (TextView) rootView.findViewById(R.id.lb_title); + if (titleView != null) { + titleView.setText(R.string.app_permissions); + } + TextView breadcrumbView = (TextView) rootView.findViewById(R.id.lb_breadcrumb); + if (breadcrumbView != null) { + breadcrumbView.setText(R.string.app_permissions_breadcrumb); + } + } + + private void updatePermissionsUi() { + Context context = getActivity(); + if (context == null) { + return; + } + + List<PermissionGroup> groups = mPermissions.getGroups(); + PreferenceScreen screen = getPreferenceScreen(); + if (screen == null) { + screen = getPreferenceManager().createPreferenceScreen(getActivity()); + setPreferenceScreen(screen); + } + + // Use this to speed up getting the info for all of the PermissionApps below. + // Create a new one for each refresh to make sure it has fresh data. + PmCache cache = new PmCache(getContext().getPackageManager()); + for (PermissionGroup group : groups) { + boolean isSystemPermission = group.getDeclaringPackage().equals(OS_PKG); + + Preference preference = findPreference(group.getName()); + if (preference == null && mExtraScreen != null) { + preference = mExtraScreen.findPreference(group.getName()); + } + if (preference == null) { + preference = new Preference(context); + preference.setOnPreferenceClickListener(this); + preference.setKey(group.getName()); + preference.setIcon(Utils.applyTint(context, group.getIcon(), + android.R.attr.colorControlNormal)); + preference.setTitle(group.getLabel()); + // Set blank summary so that no resizing/jumping happens when the summary is loaded. + preference.setSummary(" "); + preference.setPersistent(false); + if (isSystemPermission) { + screen.addPreference(preference); + } else { + if (mExtraScreen == null) { + mExtraScreen = getPreferenceManager().createPreferenceScreen(context); + } + mExtraScreen.addPreference(preference); + } + } + final Preference finalPref = preference; + + new PermissionApps(getContext(), group.getName(), new PermissionApps.Callback() { + @Override + public void onPermissionsLoaded(PermissionApps permissionApps) { + if (getActivity() == null) { + return; + } + int granted = permissionApps.getGrantedCount(mLauncherPkgs); + int total = permissionApps.getTotalCount(mLauncherPkgs); + finalPref.setSummary(getString(R.string.app_permissions_group_summary, + granted, total)); + } + }, cache).refresh(false); + } + + if (mExtraScreen != null && mExtraScreen.getPreferenceCount() > 0 + && screen.findPreference(EXTRA_PREFS_KEY) == null) { + Preference extraScreenPreference = new Preference(context); + extraScreenPreference.setKey(EXTRA_PREFS_KEY); + extraScreenPreference.setIcon(Utils.applyTint(context, + R.drawable.ic_more_items, + android.R.attr.colorControlNormal)); + extraScreenPreference.setTitle(R.string.additional_permissions); + extraScreenPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + AdditionalPermissionsFragment frag = new AdditionalPermissionsFragment(); + frag.setTargetFragment(ManagePermissionsFragment.this, 0); + FragmentTransaction ft = getFragmentManager().beginTransaction(); + ft.replace(android.R.id.content, frag); + ft.addToBackStack(null); + ft.commit(); + return true; + } + }); + int count = mExtraScreen.getPreferenceCount(); + extraScreenPreference.setSummary(getResources().getQuantityString( + R.plurals.additional_permissions_more, count, count)); + screen.addPreference(extraScreenPreference); + } + if (screen.getPreferenceCount() != 0) { + setLoading(false /* loading */, true /* animate */); + } + } + + public static class AdditionalPermissionsFragment extends PermissionsFrameFragment { + @Override + public void onCreate(Bundle icicle) { + setLoading(true /* loading */, false /* animate */); + super.onCreate(icicle); + getActivity().setTitle(R.string.additional_permissions); + setHasOptionsMenu(true); + + setPreferenceScreen(((ManagePermissionsFragment) getTargetFragment()).mExtraScreen); + setLoading(false /* loading */, true /* animate */); + } + + @Override + public void onDestroy() { + getActivity().setTitle(R.string.app_permissions); + super.onDestroy(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + getFragmentManager().popBackStack(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + bindPermissionUi(getActivity(), getView()); + } + } +} diff --git a/src/com/android/packageinstaller/permission/ui/handheld/PermissionAppsFragment.java b/src/com/android/packageinstaller/permission/ui/handheld/PermissionAppsFragment.java new file mode 100644 index 00000000..eee2f716 --- /dev/null +++ b/src/com/android/packageinstaller/permission/ui/handheld/PermissionAppsFragment.java @@ -0,0 +1,428 @@ +/* + * 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.handheld; + +import android.app.ActionBar; +import android.app.AlertDialog; +import android.app.Fragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import com.android.packageinstaller.DeviceUtils; +import com.android.packageinstaller.R; +import com.android.packageinstaller.permission.model.AppPermissionGroup; +import com.android.packageinstaller.permission.model.PermissionApps; +import com.android.packageinstaller.permission.model.PermissionApps.Callback; +import com.android.packageinstaller.permission.model.PermissionApps.PermissionApp; +import com.android.packageinstaller.permission.ui.OverlayTouchActivity; +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 PermissionAppsFragment extends PermissionsFrameFragment implements Callback, + Preference.OnPreferenceChangeListener { + + private static final int MENU_SHOW_SYSTEM = Menu.FIRST; + private static final int MENU_HIDE_SYSTEM = Menu.FIRST + 1; + private static final String KEY_SHOW_SYSTEM_PREFS = "_showSystem"; + + public static PermissionAppsFragment newInstance(String permissionName) { + return setPermissionName(new PermissionAppsFragment(), permissionName); + } + + private static <T extends Fragment> T setPermissionName(T fragment, String permissionName) { + Bundle arguments = new Bundle(); + arguments.putString(Intent.EXTRA_PERMISSION_NAME, permissionName); + fragment.setArguments(arguments); + return fragment; + } + + private PermissionApps mPermissionApps; + + private PreferenceScreen mExtraScreen; + + private ArrayMap<String, AppPermissionGroup> mToggledGroups; + private ArraySet<String> mLauncherPkgs; + private boolean mHasConfirmedRevoke; + + private boolean mShowSystem; + private MenuItem mShowSystemMenu; + private MenuItem mHideSystemMenu; + + private Callback mOnPermissionsLoadedListener; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setLoading(true /* loading */, false /* animate */); + setHasOptionsMenu(true); + final ActionBar ab = getActivity().getActionBar(); + if (ab != null) { + ab.setDisplayHomeAsUpEnabled(true); + } + mLauncherPkgs = Utils.getLauncherPackages(getContext()); + + String groupName = getArguments().getString(Intent.EXTRA_PERMISSION_NAME); + mPermissionApps = new PermissionApps(getActivity(), groupName, this); + mPermissionApps.refresh(true); + } + + @Override + public void onResume() { + super.onResume(); + mPermissionApps.refresh(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + mShowSystemMenu = menu.add(Menu.NONE, MENU_SHOW_SYSTEM, Menu.NONE, + R.string.menu_show_system); + mHideSystemMenu = menu.add(Menu.NONE, MENU_HIDE_SYSTEM, Menu.NONE, + R.string.menu_hide_system); + updateMenu(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + getActivity().finish(); + return true; + case MENU_SHOW_SYSTEM: + case MENU_HIDE_SYSTEM: + mShowSystem = item.getItemId() == MENU_SHOW_SYSTEM; + if (mPermissionApps.getApps() != null) { + onPermissionsLoaded(mPermissionApps); + } + updateMenu(); + break; + } + return super.onOptionsItemSelected(item); + } + + private void updateMenu() { + mShowSystemMenu.setVisible(!mShowSystem); + mHideSystemMenu.setVisible(mShowSystem); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + bindUi(this, mPermissionApps); + } + + private static void bindUi(Fragment fragment, PermissionApps permissionApps) { + final Drawable icon = permissionApps.getIcon(); + final CharSequence label = permissionApps.getLabel(); + final ActionBar ab = fragment.getActivity().getActionBar(); + if (ab != null) { + ab.setTitle(fragment.getString(R.string.permission_title, label)); + } + + final ViewGroup rootView = (ViewGroup) fragment.getView(); + final ImageView iconView = (ImageView) rootView.findViewById(R.id.lb_icon); + if (iconView != null) { + // Set the icon as the background instead of the image because ImageView + // doesn't properly scale vector drawables beyond their intrinsic size + iconView.setBackground(icon); + } + final TextView titleView = (TextView) rootView.findViewById(R.id.lb_title); + if (titleView != null) { + titleView.setText(label); + } + final TextView breadcrumbView = (TextView) rootView.findViewById(R.id.lb_breadcrumb); + if (breadcrumbView != null) { + breadcrumbView.setText(R.string.app_permissions); + } + } + + private void setOnPermissionsLoadedListener(Callback callback) { + mOnPermissionsLoadedListener = callback; + } + + @Override + public void onPermissionsLoaded(PermissionApps permissionApps) { + Context context = getActivity(); + + if (context == null) { + return; + } + + boolean isTelevision = DeviceUtils.isTelevision(context); + PreferenceScreen screen = getPreferenceScreen(); + if (screen == null) { + screen = getPreferenceManager().createPreferenceScreen(getActivity()); + setPreferenceScreen(screen); + } + + ArraySet<String> preferencesToRemove = new ArraySet<>(); + for (int i = 0, n = screen.getPreferenceCount(); i < n; i++) { + preferencesToRemove.add(screen.getPreference(i).getKey()); + } + if (mExtraScreen != null) { + for (int i = 0, n = mExtraScreen.getPreferenceCount(); i < n; i++) { + preferencesToRemove.add(mExtraScreen.getPreference(i).getKey()); + } + } + + for (PermissionApp app : permissionApps.getApps()) { + if (!Utils.shouldShowPermission(app)) { + continue; + } + + String key = app.getKey(); + preferencesToRemove.remove(key); + Preference existingPref = screen.findPreference(key); + if (existingPref == null && mExtraScreen != null) { + existingPref = mExtraScreen.findPreference(key); + } + + boolean isSystemApp = Utils.isSystem(app, mLauncherPkgs); + if (isSystemApp && !isTelevision && !mShowSystem) { + if (existingPref != null) { + screen.removePreference(existingPref); + } + continue; + } + + if (existingPref != null) { + // If existing preference - only update its state. + if (app.isPolicyFixed()) { + existingPref.setSummary(getString( + R.string.permission_summary_enforced_by_policy)); + } + existingPref.setPersistent(false); + existingPref.setEnabled(!app.isPolicyFixed()); + if (existingPref instanceof SwitchPreference) { + ((SwitchPreference) existingPref) + .setChecked(app.areRuntimePermissionsGranted()); + } + continue; + } + + SwitchPreference pref = new SwitchPreference(context); + pref.setOnPreferenceChangeListener(this); + pref.setKey(app.getKey()); + pref.setIcon(app.getIcon()); + pref.setTitle(app.getLabel()); + if (app.isPolicyFixed()) { + pref.setSummary(getString(R.string.permission_summary_enforced_by_policy)); + } + pref.setPersistent(false); + pref.setEnabled(!app.isPolicyFixed()); + pref.setChecked(app.areRuntimePermissionsGranted()); + + if (isSystemApp && isTelevision) { + if (mExtraScreen == null) { + mExtraScreen = getPreferenceManager().createPreferenceScreen(context); + } + mExtraScreen.addPreference(pref); + } else { + screen.addPreference(pref); + } + } + + if (mExtraScreen != null) { + preferencesToRemove.remove(KEY_SHOW_SYSTEM_PREFS); + Preference pref = screen.findPreference(KEY_SHOW_SYSTEM_PREFS); + + if (pref == null) { + pref = new Preference(context); + pref.setKey(KEY_SHOW_SYSTEM_PREFS); + pref.setIcon(Utils.applyTint(context, R.drawable.ic_toc, + android.R.attr.colorControlNormal)); + pref.setTitle(R.string.preference_show_system_apps); + pref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + SystemAppsFragment frag = new SystemAppsFragment(); + setPermissionName(frag, getArguments().getString(Intent.EXTRA_PERMISSION_NAME)); + frag.setTargetFragment(PermissionAppsFragment.this, 0); + getFragmentManager().beginTransaction() + .replace(android.R.id.content, frag) + .addToBackStack("SystemApps") + .commit(); + return true; + } + }); + screen.addPreference(pref); + } + + int grantedCount = 0; + for (int i = 0, n = mExtraScreen.getPreferenceCount(); i < n; i++) { + if (((SwitchPreference) mExtraScreen.getPreference(i)).isChecked()) { + grantedCount++; + } + } + pref.setSummary(getString(R.string.app_permissions_group_summary, + grantedCount, mExtraScreen.getPreferenceCount())); + } + + for (String key : preferencesToRemove) { + Preference pref = screen.findPreference(key); + if (pref != null) { + screen.removePreference(pref); + } else if (mExtraScreen != null) { + pref = mExtraScreen.findPreference(key); + if (pref != null) { + mExtraScreen.removePreference(pref); + } + } + } + + setLoading(false /* loading */, true /* animate */); + + if (mOnPermissionsLoadedListener != null) { + mOnPermissionsLoadedListener.onPermissionsLoaded(permissionApps); + } + } + + @Override + public boolean onPreferenceChange(final Preference preference, Object newValue) { + String pkg = preference.getKey(); + final PermissionApp app = mPermissionApps.getApp(pkg); + + if (app == null) { + return false; + } + + OverlayTouchActivity activity = (OverlayTouchActivity) getActivity(); + if (activity.isObscuredTouch()) { + activity.showOverlayDialog(); + return false; + } + + addToggledGroup(app.getPackageName(), app.getPermissionGroup()); + + if (LocationUtils.isLocationGroupAndProvider(mPermissionApps.getGroupName(), + app.getPackageName())) { + LocationUtils.showLocationDialog(getContext(), app.getLabel()); + return false; + } + if (newValue == Boolean.TRUE) { + app.grantRuntimePermissions(); + } else { + final boolean grantedByDefault = app.hasGrantedByDefaultPermissions(); + if (grantedByDefault || (!app.hasRuntimePermissions() && !mHasConfirmedRevoke)) { + new AlertDialog.Builder(getContext()) + .setMessage(grantedByDefault ? R.string.system_warning + : R.string.old_sdk_deny_warning) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.grant_dialog_button_deny, + new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + ((SwitchPreference) preference).setChecked(false); + app.revokeRuntimePermissions(); + if (!grantedByDefault) { + mHasConfirmedRevoke = true; + } + } + }) + .show(); + return false; + } else { + app.revokeRuntimePermissions(); + } + } + return true; + } + + @Override + public void onPause() { + super.onPause(); + logToggledGroups(); + } + + private void addToggledGroup(String packageName, AppPermissionGroup group) { + if (mToggledGroups == null) { + mToggledGroups = new ArrayMap<>(); + } + // Double toggle is back to initial state. + if (mToggledGroups.containsKey(packageName)) { + mToggledGroups.remove(packageName); + } else { + mToggledGroups.put(packageName, group); + } + } + + private void logToggledGroups() { + if (mToggledGroups != null) { + final int groupCount = mToggledGroups.size(); + for (int i = 0; i < groupCount; i++) { + String packageName = mToggledGroups.keyAt(i); + List<AppPermissionGroup> groups = new ArrayList<>(); + groups.add(mToggledGroups.valueAt(i)); + SafetyNetLogger.logPermissionsToggled(packageName, groups); + } + mToggledGroups = null; + } + } + + public static class SystemAppsFragment extends PermissionsFrameFragment implements Callback { + PermissionAppsFragment mOuterFragment; + + @Override + public void onCreate(Bundle savedInstanceState) { + mOuterFragment = (PermissionAppsFragment) getTargetFragment(); + setLoading(true /* loading */, false /* animate */); + super.onCreate(savedInstanceState); + if (mOuterFragment.mExtraScreen != null) { + setPreferenceScreen(); + } else { + mOuterFragment.setOnPermissionsLoadedListener(this); + } + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + String groupName = getArguments().getString(Intent.EXTRA_PERMISSION_NAME); + PermissionApps permissionApps = new PermissionApps(getActivity(), groupName, null); + bindUi(this, permissionApps); + } + + @Override + public void onPermissionsLoaded(PermissionApps permissionApps) { + setPreferenceScreen(); + mOuterFragment.setOnPermissionsLoadedListener(null); + } + + private void setPreferenceScreen() { + setPreferenceScreen(mOuterFragment.mExtraScreen); + setLoading(false /* loading */, true /* animate */); + } + } +} diff --git a/src/com/android/packageinstaller/permission/ui/handheld/PermissionsFrameFragment.java b/src/com/android/packageinstaller/permission/ui/handheld/PermissionsFrameFragment.java new file mode 100644 index 00000000..e7f63b23 --- /dev/null +++ b/src/com/android/packageinstaller/permission/ui/handheld/PermissionsFrameFragment.java @@ -0,0 +1,121 @@ +/* + * 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.handheld; + +import android.os.Bundle; +import android.preference.PreferenceFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.Animation.AnimationListener; +import android.view.animation.AnimationUtils; +import android.widget.ListView; +import android.widget.TextView; +import com.android.packageinstaller.R; + +public abstract class PermissionsFrameFragment extends PreferenceFragment { + private ViewGroup mPreferencesContainer; + + private View mLoadingView; + private ViewGroup mPrefsView; + private boolean mIsLoading; + + /** + * Returns the view group that holds the preferences objects. This will + * only be set after {@link #onCreateView} has been called. + */ + protected final ViewGroup getPreferencesContainer() { + return mPreferencesContainer; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.permissions_frame, container, + false); + mPrefsView = (ViewGroup) rootView.findViewById(R.id.prefs_container); + if (mPrefsView == null) { + mPrefsView = rootView; + } + mLoadingView = rootView.findViewById(R.id.loading_container); + mPreferencesContainer = (ViewGroup) super.onCreateView( + inflater, mPrefsView, savedInstanceState); + setLoading(mIsLoading, false, true /* force */); + mPrefsView.addView(mPreferencesContainer); + return rootView; + } + + protected void setLoading(boolean loading, boolean animate) { + setLoading(loading, animate, false); + } + + private void setLoading(boolean loading, boolean animate, boolean force) { + if (mIsLoading != loading || force) { + mIsLoading = loading; + if (getView() == null) { + // If there is no created view, there is no reason to animate. + animate = false; + } + if (mPrefsView != null) { + setViewShown(mPrefsView, !loading, animate); + } + if (mLoadingView != null) { + setViewShown(mLoadingView, loading, animate); + } + } + } + + @Override + public ListView getListView() { + ListView listView = super.getListView(); + if (listView.getEmptyView() == null) { + TextView emptyView = (TextView) getView().findViewById(R.id.no_permissions); + listView.setEmptyView(emptyView); + } + return listView; + } + + private void setViewShown(final View view, boolean shown, boolean animate) { + if (animate) { + Animation animation = AnimationUtils.loadAnimation(getContext(), + shown ? android.R.anim.fade_in : android.R.anim.fade_out); + if (shown) { + view.setVisibility(View.VISIBLE); + } else { + animation.setAnimationListener(new AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + view.setVisibility(View.INVISIBLE); + } + }); + } + view.startAnimation(animation); + } else { + view.clearAnimation(); + view.setVisibility(shown ? View.VISIBLE : View.INVISIBLE); + } + } +} diff --git a/src/com/android/packageinstaller/permission/ui/SettingsWithHeader.java b/src/com/android/packageinstaller/permission/ui/handheld/SettingsWithHeader.java index 7b58fed1..c15a4287 100644 --- a/src/com/android/packageinstaller/permission/ui/SettingsWithHeader.java +++ b/src/com/android/packageinstaller/permission/ui/handheld/SettingsWithHeader.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.packageinstaller.permission.ui; +package com.android.packageinstaller.permission.ui.handheld; import android.content.Intent; import android.graphics.drawable.Drawable; @@ -26,6 +26,7 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import com.android.packageinstaller.DeviceUtils; import com.android.packageinstaller.R; import com.android.packageinstaller.permission.utils.Utils; @@ -42,7 +43,7 @@ public abstract class SettingsWithHeader extends PermissionsFrameFragment Bundle savedInstanceState) { ViewGroup root = (ViewGroup) super.onCreateView(inflater, container, savedInstanceState); - if (!Utils.isTelevision(getContext())) { + if (!DeviceUtils.isTelevision(getContext())) { mHeader = inflater.inflate(R.layout.header, root, false); getPreferencesContainer().addView(mHeader, 0); updateHeader(); @@ -81,5 +82,4 @@ public abstract class SettingsWithHeader extends PermissionsFrameFragment public void onClick(View v) { getActivity().startActivity(mInfoIntent); } - } diff --git a/src/com/android/packageinstaller/permission/ui/AllAppPermissionsFragment.java b/src/com/android/packageinstaller/permission/ui/television/AllAppPermissionsFragment.java index 2fb9a510..d4910128 100644 --- a/src/com/android/packageinstaller/permission/ui/AllAppPermissionsFragment.java +++ b/src/com/android/packageinstaller/permission/ui/television/AllAppPermissionsFragment.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.packageinstaller.permission.ui; +package com.android.packageinstaller.permission.ui.television; import android.app.ActionBar; import android.app.AlertDialog; diff --git a/src/com/android/packageinstaller/permission/ui/AppPermissionsFragment.java b/src/com/android/packageinstaller/permission/ui/television/AppPermissionsFragment.java index 6396c61e..42a2661c 100644 --- a/src/com/android/packageinstaller/permission/ui/AppPermissionsFragment.java +++ b/src/com/android/packageinstaller/permission/ui/television/AppPermissionsFragment.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.packageinstaller.permission.ui; +package com.android.packageinstaller.permission.ui.television; import android.annotation.Nullable; import android.app.ActionBar; @@ -50,6 +50,7 @@ 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.utils.LocationUtils; import com.android.packageinstaller.permission.utils.SafetyNetLogger; import com.android.packageinstaller.permission.utils.Utils; diff --git a/src/com/android/packageinstaller/permission/ui/GrantPermissionsTvViewHandler.java b/src/com/android/packageinstaller/permission/ui/television/GrantPermissionsViewHandlerImpl.java index 0e979ab6..a2538821 100644 --- a/src/com/android/packageinstaller/permission/ui/GrantPermissionsTvViewHandler.java +++ b/src/com/android/packageinstaller/permission/ui/television/GrantPermissionsViewHandlerImpl.java @@ -1,4 +1,4 @@ -package com.android.packageinstaller.permission.ui; +package com.android.packageinstaller.permission.ui.television; import android.content.Context; import android.graphics.PixelFormat; @@ -15,11 +15,12 @@ import android.widget.LinearLayout; import android.widget.TextView; import com.android.packageinstaller.R; +import com.android.packageinstaller.permission.ui.GrantPermissionsViewHandler; /** * TV-specific view handler for the grant permissions activity. */ -final class GrantPermissionsTvViewHandler implements GrantPermissionsViewHandler, OnClickListener { +public final class GrantPermissionsViewHandlerImpl implements GrantPermissionsViewHandler, OnClickListener { private static final String ARG_GROUP_NAME = "ARG_GROUP_NAME"; @@ -37,12 +38,12 @@ final class GrantPermissionsTvViewHandler implements GrantPermissionsViewHandler private Button mSoftDenyButton; private Button mHardDenyButton; - GrantPermissionsTvViewHandler(Context context) { + public GrantPermissionsViewHandlerImpl(Context context) { mContext = context; } @Override - public GrantPermissionsTvViewHandler setResultListener(ResultListener listener) { + public GrantPermissionsViewHandlerImpl setResultListener(ResultListener listener) { mResultListener = listener; return this; } diff --git a/src/com/android/packageinstaller/permission/ui/ManagePermissionsFragment.java b/src/com/android/packageinstaller/permission/ui/television/ManagePermissionsFragment.java index e5e06e09..47301f48 100644 --- a/src/com/android/packageinstaller/permission/ui/ManagePermissionsFragment.java +++ b/src/com/android/packageinstaller/permission/ui/television/ManagePermissionsFragment.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.packageinstaller.permission.ui; +package com.android.packageinstaller.permission.ui.television; import android.annotation.Nullable; import android.app.ActionBar; @@ -202,7 +202,7 @@ public final class ManagePermissionsFragment extends PermissionsFrameFragment Preference extraScreenPreference = new Preference(context); extraScreenPreference.setKey(EXTRA_PREFS_KEY); extraScreenPreference.setIcon(Utils.applyTint(context, - com.android.internal.R.drawable.ic_more_items, + R.drawable.ic_more_items, android.R.attr.colorControlNormal)); extraScreenPreference.setTitle(R.string.additional_permissions); extraScreenPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() { diff --git a/src/com/android/packageinstaller/permission/ui/PermissionAppsFragment.java b/src/com/android/packageinstaller/permission/ui/television/PermissionAppsFragment.java index 8dacd037..0f240bef 100644 --- a/src/com/android/packageinstaller/permission/ui/PermissionAppsFragment.java +++ b/src/com/android/packageinstaller/permission/ui/television/PermissionAppsFragment.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.packageinstaller.permission.ui; +package com.android.packageinstaller.permission.ui.television; import android.app.ActionBar; import android.app.AlertDialog; @@ -39,11 +39,13 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import com.android.packageinstaller.DeviceUtils; import com.android.packageinstaller.R; import com.android.packageinstaller.permission.model.AppPermissionGroup; import com.android.packageinstaller.permission.model.PermissionApps; import com.android.packageinstaller.permission.model.PermissionApps.Callback; import com.android.packageinstaller.permission.model.PermissionApps.PermissionApp; +import com.android.packageinstaller.permission.ui.OverlayTouchActivity; import com.android.packageinstaller.permission.utils.LocationUtils; import com.android.packageinstaller.permission.utils.SafetyNetLogger; import com.android.packageinstaller.permission.utils.Utils; @@ -185,7 +187,7 @@ public final class PermissionAppsFragment extends PermissionsFrameFragment imple return; } - boolean isTelevision = Utils.isTelevision(context); + boolean isTelevision = DeviceUtils.isTelevision(context); PreferenceScreen screen = getPreferenceScreen(); ArraySet<String> preferencesToRemove = new ArraySet<>(); diff --git a/src/com/android/packageinstaller/permission/ui/PermissionsFrameFragment.java b/src/com/android/packageinstaller/permission/ui/television/PermissionsFrameFragment.java index 40058f6d..e81aee86 100644 --- a/src/com/android/packageinstaller/permission/ui/PermissionsFrameFragment.java +++ b/src/com/android/packageinstaller/permission/ui/television/PermissionsFrameFragment.java @@ -1,4 +1,20 @@ -package com.android.packageinstaller.permission.ui; +/* + * 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.television; import android.annotation.Nullable; import android.os.Bundle; @@ -15,6 +31,7 @@ import android.view.animation.Animation.AnimationListener; import android.view.animation.AnimationUtils; import android.widget.TextView; +import com.android.packageinstaller.DeviceUtils; import com.android.packageinstaller.R; import com.android.packageinstaller.permission.utils.Utils; @@ -117,7 +134,7 @@ public abstract class PermissionsFrameFragment extends PreferenceFragment { @Override public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { - if (Utils.isTelevision(getContext())) { + if (DeviceUtils.isTelevision(getContext())) { mGridView = (VerticalGridView) inflater.inflate( R.layout.leanback_preferences_list, parent, false); mGridView.setWindowAlignmentOffset(0); diff --git a/src/com/android/packageinstaller/permission/ui/television/SettingsWithHeader.java b/src/com/android/packageinstaller/permission/ui/television/SettingsWithHeader.java new file mode 100644 index 00000000..4dae629c --- /dev/null +++ b/src/com/android/packageinstaller/permission/ui/television/SettingsWithHeader.java @@ -0,0 +1,86 @@ +/* + * 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.television; + +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.packageinstaller.DeviceUtils; +import com.android.packageinstaller.R; +import com.android.packageinstaller.permission.utils.Utils; + +public abstract class SettingsWithHeader extends PermissionsFrameFragment + implements OnClickListener { + + private View mHeader; + protected Intent mInfoIntent; + protected Drawable mIcon; + protected CharSequence mLabel; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + ViewGroup root = (ViewGroup) super.onCreateView(inflater, container, savedInstanceState); + + if (!DeviceUtils.isTelevision(getContext())) { + mHeader = inflater.inflate(R.layout.header, root, false); + getPreferencesContainer().addView(mHeader, 0); + updateHeader(); + } + + return root; + } + + public void setHeader(Drawable icon, CharSequence label, Intent infoIntent) { + mIcon = icon; + mLabel = label; + mInfoIntent = infoIntent; + updateHeader(); + } + + private void updateHeader() { + if (mHeader != null) { + final ImageView appIcon = (ImageView) mHeader.findViewById(R.id.icon); + appIcon.setImageDrawable(mIcon); + + final TextView appName = (TextView) mHeader.findViewById(R.id.name); + appName.setText(mLabel); + + final View info = mHeader.findViewById(R.id.info); + if (mInfoIntent == null) { + info.setVisibility(View.GONE); + } else { + info.setVisibility(View.VISIBLE); + info.setClickable(true); + info.setOnClickListener(this); + } + } + } + + @Override + public void onClick(View v) { + getActivity().startActivity(mInfoIntent); + } + +} 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); + } + } + } +} |