diff options
Diffstat (limited to 'src')
43 files changed, 2270 insertions, 1702 deletions
diff --git a/src/com/android/packageinstaller/EventLogTags.logtags b/src/com/android/packageinstaller/EventLogTags.logtags deleted file mode 100644 index 8cbb1ccd..00000000 --- a/src/com/android/packageinstaller/EventLogTags.logtags +++ /dev/null @@ -1,6 +0,0 @@ -# See system/core/logcat/event.logtags for a description of the format of this file. - -option java_package com.android.packageinstaller - -# APK install attempt via PackageInstaller (see InstallFlowAnalytics for format) -90300 install_package_attempt (result_and_flags|1),(total_time|1|3),(time_till_pkg_info_obtained|1|3),(time_till_install_clicked|1|3),(package_digest|3) diff --git a/src/com/android/packageinstaller/InstallAppProgress.java b/src/com/android/packageinstaller/InstallAppProgress.java index d51cab1d..7554704b 100755 --- a/src/com/android/packageinstaller/InstallAppProgress.java +++ b/src/com/android/packageinstaller/InstallAppProgress.java @@ -16,32 +16,43 @@ */ package com.android.packageinstaller; +import static android.content.pm.PackageInstaller.SessionParams.UID_UNKNOWN; + import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; +import android.app.PendingIntent; +import android.content.ActivityNotFoundException; +import android.content.BroadcastReceiver; +import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ApplicationInfo; -import android.content.pm.IPackageInstallObserver; -import android.content.pm.ManifestDigest; import android.content.pm.PackageInfo; +import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; -import android.content.pm.VerificationParams; -import android.graphics.drawable.LevelListDrawable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.os.HandlerThread; import android.os.Message; import android.util.Log; import android.view.View; import android.widget.Button; +import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; +import com.android.packageinstaller.permission.utils.IoUtils; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.List; /** @@ -54,14 +65,12 @@ import java.util.List; */ public class InstallAppProgress extends Activity implements View.OnClickListener, OnCancelListener { private final String TAG="InstallAppProgress"; - private boolean localLOGV = false; - static final String EXTRA_MANIFEST_DIGEST = - "com.android.packageinstaller.extras.manifest_digest"; - static final String EXTRA_INSTALL_FLOW_ANALYTICS = - "com.android.packageinstaller.extras.install_flow_analytics"; + private static final String BROADCAST_ACTION = + "com.android.packageinstaller.ACTION_INSTALL_COMMIT"; + private static final String BROADCAST_SENDER_PERMISSION = + "android.permission.INSTALL_PACKAGES"; private ApplicationInfo mAppInfo; private Uri mPackageURI; - private InstallFlowAnalytics mInstallFlowAnalytics; private ProgressBar mProgressBar; private View mOkPanel; private TextView mStatusTextView; @@ -72,31 +81,31 @@ public class InstallAppProgress extends Activity implements View.OnClickListener private Intent mLaunchIntent; private static final int DLG_OUT_OF_SPACE = 1; private CharSequence mLabel; + private HandlerThread mInstallThread; + private Handler mInstallHandler; private Handler mHandler = new Handler() { public void handleMessage(Message msg) { switch (msg.what) { case INSTALL_COMPLETE: - mInstallFlowAnalytics.setFlowFinishedWithPackageManagerResult(msg.arg1); if (getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) { Intent result = new Intent(); result.putExtra(Intent.EXTRA_INSTALL_RESULT, msg.arg1); - setResult(msg.arg1 == PackageManager.INSTALL_SUCCEEDED + setResult(msg.arg1 == PackageInstaller.STATUS_SUCCESS ? Activity.RESULT_OK : Activity.RESULT_FIRST_USER, result); - finish(); + clearCachedApkIfNeededAndFinish(); return; } // Update the status text - mProgressBar.setVisibility(View.INVISIBLE); + mProgressBar.setVisibility(View.GONE); // Show the ok button int centerTextLabel; int centerExplanationLabel = -1; - LevelListDrawable centerTextDrawable = - (LevelListDrawable) getDrawable(R.drawable.ic_result_status); - if (msg.arg1 == PackageManager.INSTALL_SUCCEEDED) { + if (msg.arg1 == PackageInstaller.STATUS_SUCCESS) { mLaunchButton.setVisibility(View.VISIBLE); - centerTextDrawable.setLevel(0); + ((ImageView)findViewById(R.id.center_icon)) + .setImageDrawable(getDrawable(R.drawable.ic_done_92)); centerTextLabel = R.string.install_done; // Enable or disable launch button mLaunchIntent = getPackageManager().getLaunchIntentForPackage( @@ -114,29 +123,26 @@ public class InstallAppProgress extends Activity implements View.OnClickListener } else { mLaunchButton.setEnabled(false); } - } else if (msg.arg1 == PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE){ + } else if (msg.arg1 == PackageInstaller.STATUS_FAILURE_STORAGE){ showDialogInner(DLG_OUT_OF_SPACE); return; } else { // Generic error handling for all other error codes. - centerTextDrawable.setLevel(1); + ((ImageView)findViewById(R.id.center_icon)) + .setImageDrawable(getDrawable(R.drawable.ic_report_problem_92)); centerExplanationLabel = getExplanationFromErrorCode(msg.arg1); centerTextLabel = R.string.install_failed; - mLaunchButton.setVisibility(View.INVISIBLE); - } - if (centerTextDrawable != null) { - centerTextDrawable.setBounds(0, 0, - centerTextDrawable.getIntrinsicWidth(), - centerTextDrawable.getIntrinsicHeight()); - mStatusTextView.setCompoundDrawablesRelative(centerTextDrawable, null, - null, null); + mLaunchButton.setVisibility(View.GONE); } - mStatusTextView.setText(centerTextLabel); if (centerExplanationLabel != -1) { mExplanationTextView.setText(centerExplanationLabel); - mExplanationTextView.setVisibility(View.VISIBLE); + findViewById(R.id.center_view).setVisibility(View.GONE); + ((TextView)findViewById(R.id.explanation_status)).setText(centerTextLabel); + findViewById(R.id.explanation_view).setVisibility(View.VISIBLE); } else { - mExplanationTextView.setVisibility(View.GONE); + ((TextView)findViewById(R.id.center_text)).setText(centerTextLabel); + findViewById(R.id.center_view).setVisibility(View.VISIBLE); + findViewById(R.id.explanation_view).setVisibility(View.GONE); } mDoneButton.setOnClickListener(InstallAppProgress.this); mOkPanel.setVisibility(View.VISIBLE); @@ -146,18 +152,31 @@ public class InstallAppProgress extends Activity implements View.OnClickListener } } }; - + + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final int statusCode = intent.getIntExtra( + PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); + if (statusCode == PackageInstaller.STATUS_PENDING_USER_ACTION) { + context.startActivity((Intent)intent.getParcelableExtra(Intent.EXTRA_INTENT)); + } else { + onPackageInstalled(statusCode); + } + } + }; + private int getExplanationFromErrorCode(int errCode) { Log.d(TAG, "Installation error code: " + errCode); switch (errCode) { - case PackageManager.INSTALL_FAILED_INVALID_APK: + case PackageInstaller.STATUS_FAILURE_BLOCKED: + return R.string.install_failed_blocked; + case PackageInstaller.STATUS_FAILURE_CONFLICT: + return R.string.install_failed_conflict; + case PackageInstaller.STATUS_FAILURE_INCOMPATIBLE: + return R.string.install_failed_incompatible; + case PackageInstaller.STATUS_FAILURE_INVALID: return R.string.install_failed_invalid_apk; - case PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES: - return R.string.install_failed_inconsistent_certificates; - case PackageManager.INSTALL_FAILED_OLDER_SDK: - return R.string.install_failed_older_sdk; - case PackageManager.INSTALL_FAILED_CPU_ABI_INCOMPATIBLE: - return R.string.install_failed_cpu_abi_incompatible; default: return -1; } @@ -168,21 +187,32 @@ public class InstallAppProgress extends Activity implements View.OnClickListener super.onCreate(icicle); Intent intent = getIntent(); mAppInfo = intent.getParcelableExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO); - mInstallFlowAnalytics = intent.getParcelableExtra(EXTRA_INSTALL_FLOW_ANALYTICS); - mInstallFlowAnalytics.setContext(this); mPackageURI = intent.getData(); final String scheme = mPackageURI.getScheme(); if (scheme != null && !"file".equals(scheme) && !"package".equals(scheme)) { - mInstallFlowAnalytics.setFlowFinished( - InstallFlowAnalytics.RESULT_FAILED_UNSUPPORTED_SCHEME); throw new IllegalArgumentException("unexpected scheme " + scheme); } + mInstallThread = new HandlerThread("InstallThread"); + mInstallThread.start(); + mInstallHandler = new Handler(mInstallThread.getLooper()); + + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(BROADCAST_ACTION); + registerReceiver( + mBroadcastReceiver, intentFilter, BROADCAST_SENDER_PERMISSION, null /*scheduler*/); + initView(); } @Override + public void onBackPressed() { + clearCachedApkIfNeededAndFinish(); + } + + @SuppressWarnings("deprecation") + @Override public Dialog onCreateDialog(int id, Bundle bundle) { switch (id) { case DLG_OUT_OF_SPACE: @@ -195,13 +225,13 @@ public class InstallAppProgress extends Activity implements View.OnClickListener //launch manage applications Intent intent = new Intent("android.intent.action.MANAGE_PACKAGE_STORAGE"); startActivity(intent); - finish(); + clearCachedApkIfNeededAndFinish(); } }) .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { Log.i(TAG, "Canceling installation"); - finish(); + clearCachedApkIfNeededAndFinish(); } }) .setOnCancelListener(this) @@ -210,36 +240,85 @@ public class InstallAppProgress extends Activity implements View.OnClickListener return null; } + @SuppressWarnings("deprecation") private void showDialogInner(int id) { removeDialog(id); showDialog(id); } - class PackageInstallObserver extends IPackageInstallObserver.Stub { - public void packageInstalled(String packageName, int returnCode) { - Message msg = mHandler.obtainMessage(INSTALL_COMPLETE); - msg.arg1 = returnCode; - mHandler.sendMessage(msg); - } + void onPackageInstalled(int statusCode) { + Message msg = mHandler.obtainMessage(INSTALL_COMPLETE); + msg.arg1 = statusCode; + mHandler.sendMessage(msg); } - public void initView() { - setContentView(R.layout.op_progress); - int installFlags = 0; + int getInstallFlags(String packageName) { PackageManager pm = getPackageManager(); try { - PackageInfo pi = pm.getPackageInfo(mAppInfo.packageName, - PackageManager.GET_UNINSTALLED_PACKAGES); - if(pi != null) { - installFlags |= PackageManager.INSTALL_REPLACE_EXISTING; + PackageInfo pi = + pm.getPackageInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES); + if (pi != null) { + return PackageManager.INSTALL_REPLACE_EXISTING; } } catch (NameNotFoundException e) { } - if((installFlags & PackageManager.INSTALL_REPLACE_EXISTING )!= 0) { - Log.w(TAG, "Replacing package:" + mAppInfo.packageName); + return 0; + } + + private void doPackageStage(PackageManager pm, PackageInstaller.SessionParams params) { + final PackageInstaller packageInstaller = pm.getPackageInstaller(); + PackageInstaller.Session session = null; + try { + final String packageLocation = mPackageURI.getPath(); + final File file = new File(packageLocation); + final int sessionId = packageInstaller.createSession(params); + final byte[] buffer = new byte[65536]; + + session = packageInstaller.openSession(sessionId); + + final InputStream in = new FileInputStream(file); + final long sizeBytes = file.length(); + final OutputStream out = session.openWrite("PackageInstaller", 0, sizeBytes); + try { + int c; + while ((c = in.read(buffer)) != -1) { + out.write(buffer, 0, c); + if (sizeBytes > 0) { + final float fraction = ((float) c / (float) sizeBytes); + session.addProgress(fraction); + } + } + session.fsync(out); + } finally { + IoUtils.closeQuietly(in); + IoUtils.closeQuietly(out); + } + + // Create a PendingIntent and use it to generate the IntentSender + Intent broadcastIntent = new Intent(BROADCAST_ACTION); + PendingIntent pendingIntent = PendingIntent.getBroadcast( + InstallAppProgress.this /*context*/, + sessionId, + broadcastIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + session.commit(pendingIntent.getIntentSender()); + } catch (IOException e) { + onPackageInstalled(PackageInstaller.STATUS_FAILURE); + } finally { + IoUtils.closeQuietly(session); } + } + + void initView() { + setContentView(R.layout.op_progress); final PackageUtil.AppSnippet as; + final PackageManager pm = getPackageManager(); + final int installFlags = getInstallFlags(mAppInfo.packageName); + + if((installFlags & PackageManager.INSTALL_REPLACE_EXISTING )!= 0) { + Log.w(TAG, "Replacing package:" + mAppInfo.packageName); + } if ("package".equals(mPackageURI.getScheme())) { as = new PackageUtil.AppSnippet(pm.getApplicationLabel(mAppInfo), pm.getApplicationIcon(mAppInfo)); @@ -250,45 +329,44 @@ public class InstallAppProgress extends Activity implements View.OnClickListener mLabel = as.label; PackageUtil.initSnippetForNewApp(this, as, R.id.app_snippet); mStatusTextView = (TextView)findViewById(R.id.center_text); - mStatusTextView.setText(R.string.installing); - mExplanationTextView = (TextView) findViewById(R.id.center_explanation); + mExplanationTextView = (TextView) findViewById(R.id.explanation); mProgressBar = (ProgressBar) findViewById(R.id.progress_bar); mProgressBar.setIndeterminate(true); // Hide button till progress is being displayed - mOkPanel = (View)findViewById(R.id.buttons_panel); + mOkPanel = findViewById(R.id.buttons_panel); mDoneButton = (Button)findViewById(R.id.done_button); mLaunchButton = (Button)findViewById(R.id.launch_button); mOkPanel.setVisibility(View.INVISIBLE); - String installerPackageName = getIntent().getStringExtra( - Intent.EXTRA_INSTALLER_PACKAGE_NAME); - Uri originatingURI = getIntent().getParcelableExtra(Intent.EXTRA_ORIGINATING_URI); - Uri referrer = getIntent().getParcelableExtra(Intent.EXTRA_REFERRER); - int originatingUid = getIntent().getIntExtra(Intent.EXTRA_ORIGINATING_UID, - VerificationParams.NO_UID); - ManifestDigest manifestDigest = getIntent().getParcelableExtra(EXTRA_MANIFEST_DIGEST); - VerificationParams verificationParams = new VerificationParams(null, originatingURI, - referrer, originatingUid, manifestDigest); - PackageInstallObserver observer = new PackageInstallObserver(); - if ("package".equals(mPackageURI.getScheme())) { try { pm.installExistingPackage(mAppInfo.packageName); - observer.packageInstalled(mAppInfo.packageName, - PackageManager.INSTALL_SUCCEEDED); + onPackageInstalled(PackageInstaller.STATUS_SUCCESS); } catch (PackageManager.NameNotFoundException e) { - observer.packageInstalled(mAppInfo.packageName, - PackageManager.INSTALL_FAILED_INVALID_APK); + onPackageInstalled(PackageInstaller.STATUS_FAILURE_INVALID); } } else { - pm.installPackageWithVerificationAndEncryption(mPackageURI, observer, installFlags, - installerPackageName, verificationParams, null); + final PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( + PackageInstaller.SessionParams.MODE_FULL_INSTALL); + params.referrerUri = getIntent().getParcelableExtra(Intent.EXTRA_REFERRER); + params.originatingUri = getIntent().getParcelableExtra(Intent.EXTRA_ORIGINATING_URI); + params.originatingUid = getIntent().getIntExtra(Intent.EXTRA_ORIGINATING_UID, + UID_UNKNOWN); + + mInstallHandler.post(new Runnable() { + @Override + public void run() { + doPackageStage(pm, params); + } + }); } } @Override protected void onDestroy() { super.onDestroy(); + unregisterReceiver(mBroadcastReceiver); + mInstallThread.getLooper().quitSafely(); } public void onClick(View v) { @@ -296,14 +374,31 @@ public class InstallAppProgress extends Activity implements View.OnClickListener if (mAppInfo.packageName != null) { Log.i(TAG, "Finished installing "+mAppInfo.packageName); } - finish(); + clearCachedApkIfNeededAndFinish(); } else if(v == mLaunchButton) { - startActivity(mLaunchIntent); - finish(); + try { + startActivity(mLaunchIntent); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "Could not start activity", e); + } + clearCachedApkIfNeededAndFinish(); } } public void onCancel(DialogInterface dialog) { + clearCachedApkIfNeededAndFinish(); + } + + private void clearCachedApkIfNeededAndFinish() { + // If we are installing from a content:// the apk is copied in the cache + // dir and passed in here. As we aren't started for a result because our + // caller needs to be able to forward the result, here we make sure the + // staging file in the cache dir is removed. + if ("file".equals(mPackageURI.getScheme()) && mPackageURI.getPath() != null + && mPackageURI.getPath().startsWith(getCacheDir().toString())) { + File file = new File(mPackageURI.getPath()); + file.delete(); + } finish(); } } diff --git a/src/com/android/packageinstaller/InstallFlowAnalytics.java b/src/com/android/packageinstaller/InstallFlowAnalytics.java deleted file mode 100644 index 4591f31c..00000000 --- a/src/com/android/packageinstaller/InstallFlowAnalytics.java +++ /dev/null @@ -1,608 +0,0 @@ -/* -** -** Copyright 2013, 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; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Parcel; -import android.os.Parcelable; -import android.os.SystemClock; -import android.provider.Settings; -import android.util.EventLog; -import android.util.Log; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import libcore.io.IoUtils; - -/** - * Analytics about an attempt to install a package via {@link PackageInstallerActivity}. - * - * <p>An instance of this class is created at the beginning of the install flow and gradually filled - * as the user progresses through the flow. When the flow terminates (regardless of the reason), - * {@link #setFlowFinished(byte)} is invoked which reports the installation attempt as an event - * to the Event Log. - */ -public class InstallFlowAnalytics implements Parcelable { - - private static final String TAG = "InstallFlowAnalytics"; - - /** Installation has not yet terminated. */ - static final byte RESULT_NOT_YET_AVAILABLE = -1; - - /** Package successfully installed. */ - static final byte RESULT_SUCCESS = 0; - - /** Installation failed because scheme unsupported. */ - static final byte RESULT_FAILED_UNSUPPORTED_SCHEME = 1; - - /** - * Installation of an APK failed because of a failure to obtain information from the provided - * APK. - */ - static final byte RESULT_FAILED_TO_GET_PACKAGE_INFO = 2; - - /** - * Installation of an already installed package into the current user profile failed because the - * specified package is not installed. - */ - static final byte RESULT_FAILED_PACKAGE_MISSING = 3; - - /** - * Installation failed because installation from unknown sources is prohibited by the Unknown - * Sources setting. - */ - static final byte RESULT_BLOCKED_BY_UNKNOWN_SOURCES_SETTING = 4; - - /** Installation cancelled by the user. */ - static final byte RESULT_CANCELLED_BY_USER = 5; - - /** - * Installation failed due to {@code PackageManager} failure. PackageManager error code is - * provided in {@link #mPackageManagerInstallResult}). - */ - static final byte RESULT_PACKAGE_MANAGER_INSTALL_FAILED = 6; - - /** - * Installation blocked since this feature is not allowed on Android Wear devices yet. - */ - static final byte RESULT_NOT_ALLOWED_ON_WEAR = 7; - - private static final int FLAG_INSTALLS_FROM_UNKNOWN_SOURCES_PERMITTED = 1 << 0; - private static final int FLAG_INSTALL_REQUEST_FROM_UNKNOWN_SOURCE = 1 << 1; - private static final int FLAG_VERIFY_APPS_ENABLED = 1 << 2; - private static final int FLAG_APP_VERIFIER_INSTALLED = 1 << 3; - private static final int FLAG_FILE_URI = 1 << 4; - private static final int FLAG_REPLACE = 1 << 5; - private static final int FLAG_SYSTEM_APP = 1 << 6; - private static final int FLAG_PACKAGE_INFO_OBTAINED = 1 << 7; - private static final int FLAG_INSTALL_BUTTON_CLICKED = 1 << 8; - private static final int FLAG_NEW_PERMISSIONS_FOUND = 1 << 9; - private static final int FLAG_PERMISSIONS_DISPLAYED = 1 << 10; - private static final int FLAG_NEW_PERMISSIONS_DISPLAYED = 1 << 11; - private static final int FLAG_ALL_PERMISSIONS_DISPLAYED = 1 << 12; - - /** - * Information about this flow expressed as a collection of flags. See {@code FLAG_...} - * constants. - */ - private int mFlags; - - /** Outcome of the flow. See {@code RESULT_...} constants. */ - private byte mResult = RESULT_NOT_YET_AVAILABLE; - - /** - * Result code returned by {@code PackageManager} to install the package or {@code 0} if - * {@code PackageManager} has not yet been invoked to install the package. - */ - private int mPackageManagerInstallResult; - - /** - * Time instant when the installation request arrived, measured in elapsed realtime - * milliseconds. See {@link SystemClock#elapsedRealtime()}. - */ - private long mStartTimestampMillis; - - /** - * Time instant when the information about the package being installed was obtained, measured in - * elapsed realtime milliseconds. See {@link SystemClock#elapsedRealtime()}. - */ - private long mPackageInfoObtainedTimestampMillis; - - /** - * Time instant when the user clicked the Install button, measured in elapsed realtime - * milliseconds. See {@link SystemClock#elapsedRealtime()}. This field is only valid if the - * Install button has been clicked, as signaled by {@link #FLAG_INSTALL_BUTTON_CLICKED}. - */ - private long mInstallButtonClickTimestampMillis; - - /** - * Time instant when this flow terminated, measured in elapsed realtime milliseconds. See - * {@link SystemClock#elapsedRealtime()}. - */ - private long mEndTimestampMillis; - - /** URI of the package being installed. */ - private String mPackageUri; - - /** Whether this attempt has been logged to the Event Log. */ - private boolean mLogged; - - private Context mContext; - - public static final Parcelable.Creator<InstallFlowAnalytics> CREATOR = - new Parcelable.Creator<InstallFlowAnalytics>() { - @Override - public InstallFlowAnalytics createFromParcel(Parcel in) { - return new InstallFlowAnalytics(in); - } - - @Override - public InstallFlowAnalytics[] newArray(int size) { - return new InstallFlowAnalytics[size]; - } - }; - - public InstallFlowAnalytics() {} - - public InstallFlowAnalytics(Parcel in) { - mFlags = in.readInt(); - mResult = in.readByte(); - mPackageManagerInstallResult = in.readInt(); - mStartTimestampMillis = in.readLong(); - mPackageInfoObtainedTimestampMillis = in.readLong(); - mInstallButtonClickTimestampMillis = in.readLong(); - mEndTimestampMillis = in.readLong(); - mPackageUri = in.readString(); - mLogged = readBoolean(in); - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mFlags); - dest.writeByte(mResult); - dest.writeInt(mPackageManagerInstallResult); - dest.writeLong(mStartTimestampMillis); - dest.writeLong(mPackageInfoObtainedTimestampMillis); - dest.writeLong(mInstallButtonClickTimestampMillis); - dest.writeLong(mEndTimestampMillis); - dest.writeString(mPackageUri); - writeBoolean(dest, mLogged); - } - - private static void writeBoolean(Parcel dest, boolean value) { - dest.writeByte((byte) (value ? 1 : 0)); - } - - private static boolean readBoolean(Parcel dest) { - return dest.readByte() != 0; - } - - @Override - public int describeContents() { - return 0; - } - - void setContext(Context context) { - mContext = context; - } - - /** Sets whether the Unknown Sources setting is checked. */ - void setInstallsFromUnknownSourcesPermitted(boolean permitted) { - setFlagState(FLAG_INSTALLS_FROM_UNKNOWN_SOURCES_PERMITTED, permitted); - } - - /** Gets whether the Unknown Sources setting is checked. */ - private boolean isInstallsFromUnknownSourcesPermitted() { - return isFlagSet(FLAG_INSTALLS_FROM_UNKNOWN_SOURCES_PERMITTED); - } - - /** Sets whether this install attempt is from an unknown source. */ - void setInstallRequestFromUnknownSource(boolean unknownSource) { - setFlagState(FLAG_INSTALL_REQUEST_FROM_UNKNOWN_SOURCE, unknownSource); - } - - /** Gets whether this install attempt is from an unknown source. */ - private boolean isInstallRequestFromUnknownSource() { - return isFlagSet(FLAG_INSTALL_REQUEST_FROM_UNKNOWN_SOURCE); - } - - /** Sets whether app verification is enabled. */ - void setVerifyAppsEnabled(boolean enabled) { - setFlagState(FLAG_VERIFY_APPS_ENABLED, enabled); - } - - /** Gets whether app verification is enabled. */ - private boolean isVerifyAppsEnabled() { - return isFlagSet(FLAG_VERIFY_APPS_ENABLED); - } - - /** Sets whether at least one app verifier is installed. */ - void setAppVerifierInstalled(boolean installed) { - setFlagState(FLAG_APP_VERIFIER_INSTALLED, installed); - } - - /** Gets whether at least one app verifier is installed. */ - private boolean isAppVerifierInstalled() { - return isFlagSet(FLAG_APP_VERIFIER_INSTALLED); - } - - /** - * Sets whether an APK file is being installed. - * - * @param fileUri {@code true} if an APK file is being installed, {@code false} if an already - * installed package is being installed to this user profile. - */ - void setFileUri(boolean fileUri) { - setFlagState(FLAG_FILE_URI, fileUri); - } - - /** - * Sets the URI of the package being installed. - */ - void setPackageUri(String packageUri) { - mPackageUri = packageUri; - } - - /** - * Gets whether an APK file is being installed. - * - * @return {@code true} if an APK file is being installed, {@code false} if an already - * installed package is being installed to this user profile. - */ - private boolean isFileUri() { - return isFlagSet(FLAG_FILE_URI); - } - - /** Sets whether this is an attempt to replace an existing package. */ - void setReplace(boolean replace) { - setFlagState(FLAG_REPLACE, replace); - } - - /** Gets whether this is an attempt to replace an existing package. */ - private boolean isReplace() { - return isFlagSet(FLAG_REPLACE); - } - - /** Sets whether the package being updated is a system package. */ - void setSystemApp(boolean systemApp) { - setFlagState(FLAG_SYSTEM_APP, systemApp); - } - - /** Gets whether the package being updated is a system package. */ - private boolean isSystemApp() { - return isFlagSet(FLAG_SYSTEM_APP); - } - - /** - * Sets whether the package being installed is requesting more permissions than the already - * installed version of the package. - */ - void setNewPermissionsFound(boolean found) { - setFlagState(FLAG_NEW_PERMISSIONS_FOUND, found); - } - - /** - * Gets whether the package being installed is requesting more permissions than the already - * installed version of the package. - */ - private boolean isNewPermissionsFound() { - return isFlagSet(FLAG_NEW_PERMISSIONS_FOUND); - } - - /** Sets whether permissions were displayed to the user. */ - void setPermissionsDisplayed(boolean displayed) { - setFlagState(FLAG_PERMISSIONS_DISPLAYED, displayed); - } - - /** Gets whether permissions were displayed to the user. */ - private boolean isPermissionsDisplayed() { - return isFlagSet(FLAG_PERMISSIONS_DISPLAYED); - } - - /** - * Sets whether new permissions were displayed to the user (if permissions were displayed at - * all). - */ - void setNewPermissionsDisplayed(boolean displayed) { - setFlagState(FLAG_NEW_PERMISSIONS_DISPLAYED, displayed); - } - - /** - * Gets whether new permissions were displayed to the user (if permissions were displayed at - * all). - */ - private boolean isNewPermissionsDisplayed() { - return isFlagSet(FLAG_NEW_PERMISSIONS_DISPLAYED); - } - - /** - * Sets whether all permissions were displayed to the user (if permissions were displayed at - * all). - */ - void setAllPermissionsDisplayed(boolean displayed) { - setFlagState(FLAG_ALL_PERMISSIONS_DISPLAYED, displayed); - } - - /** - * Gets whether all permissions were displayed to the user (if permissions were displayed at - * all). - */ - private boolean isAllPermissionsDisplayed() { - return isFlagSet(FLAG_ALL_PERMISSIONS_DISPLAYED); - } - - /** - * Sets the time instant when the installation request arrived, measured in elapsed realtime - * milliseconds. See {@link SystemClock#elapsedRealtime()}. - */ - void setStartTimestampMillis(long timestampMillis) { - mStartTimestampMillis = timestampMillis; - } - - /** - * Records that the information about the package info has been obtained or that there has been - * a failure to obtain the information. - */ - void setPackageInfoObtained() { - setFlagState(FLAG_PACKAGE_INFO_OBTAINED, true); - mPackageInfoObtainedTimestampMillis = SystemClock.elapsedRealtime(); - } - - /** - * Checks whether the information about the package info has been obtained or that there has - * been a failure to obtain the information. - */ - private boolean isPackageInfoObtained() { - return isFlagSet(FLAG_PACKAGE_INFO_OBTAINED); - } - - /** - * Records that the Install button has been clicked. - */ - void setInstallButtonClicked() { - setFlagState(FLAG_INSTALL_BUTTON_CLICKED, true); - mInstallButtonClickTimestampMillis = SystemClock.elapsedRealtime(); - } - - /** - * Checks whether the Install button has been clicked. - */ - private boolean isInstallButtonClicked() { - return isFlagSet(FLAG_INSTALL_BUTTON_CLICKED); - } - - /** - * Marks this flow as finished due to {@code PackageManager} succeeding or failing to install - * the package and reports this to the Event Log. - */ - void setFlowFinishedWithPackageManagerResult(int packageManagerResult) { - mPackageManagerInstallResult = packageManagerResult; - if (packageManagerResult == PackageManager.INSTALL_SUCCEEDED) { - setFlowFinished( - InstallFlowAnalytics.RESULT_SUCCESS); - } else { - setFlowFinished( - InstallFlowAnalytics.RESULT_PACKAGE_MANAGER_INSTALL_FAILED); - } - } - - /** - * Marks this flow as finished and reports this to the Event Log. - */ - void setFlowFinished(byte result) { - if (mLogged) { - return; - } - mResult = result; - mEndTimestampMillis = SystemClock.elapsedRealtime(); - writeToEventLog(); - } - - private void writeToEventLog() { - byte packageManagerInstallResultByte = 0; - if (mResult == RESULT_PACKAGE_MANAGER_INSTALL_FAILED) { - // PackageManager install error codes are negative, starting from -1 and going to - // -111 (at the moment). We thus store them in negated form. - packageManagerInstallResultByte = clipUnsignedValueToUnsignedByte( - -mPackageManagerInstallResult); - } - - final int resultAndFlags = (mResult & 0xff) - | ((packageManagerInstallResultByte & 0xff) << 8) - | ((mFlags & 0xffff) << 16); - - // Total elapsed time from start to end, in milliseconds. - final int totalElapsedTime = - clipUnsignedLongToUnsignedInt(mEndTimestampMillis - mStartTimestampMillis); - - // Total elapsed time from start till information about the package being installed was - // obtained, in milliseconds. - final int elapsedTimeTillPackageInfoObtained = (isPackageInfoObtained()) - ? clipUnsignedLongToUnsignedInt( - mPackageInfoObtainedTimestampMillis - mStartTimestampMillis) - : 0; - - // Total elapsed time from start till Install button clicked, in milliseconds - // milliseconds. - final int elapsedTimeTillInstallButtonClick = (isInstallButtonClicked()) - ? clipUnsignedLongToUnsignedInt( - mInstallButtonClickTimestampMillis - mStartTimestampMillis) - : 0; - - // If this user has consented to app verification, augment the logged event with the hash of - // the contents of the APK. - if (((mFlags & FLAG_FILE_URI) != 0) - && ((mFlags & FLAG_VERIFY_APPS_ENABLED) != 0) - && (isUserConsentToVerifyAppsGranted())) { - // Log the hash of the APK's contents. - // Reading the APK may take a while -- perform in background. - AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() { - @Override - public void run() { - byte[] digest = null; - try { - digest = getPackageContentsDigest(); - } catch (IOException e) { - Log.w(TAG, "Failed to hash APK contents", e); - } finally { - String digestHex = (digest != null) - ? IntegralToString.bytesToHexString(digest, false) - : ""; - EventLogTags.writeInstallPackageAttempt( - resultAndFlags, - totalElapsedTime, - elapsedTimeTillPackageInfoObtained, - elapsedTimeTillInstallButtonClick, - digestHex); - } - } - }); - } else { - // Do not log the hash of the APK's contents - EventLogTags.writeInstallPackageAttempt( - resultAndFlags, - totalElapsedTime, - elapsedTimeTillPackageInfoObtained, - elapsedTimeTillInstallButtonClick, - ""); - } - mLogged = true; - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Analytics:" - + "\n\tinstallsFromUnknownSourcesPermitted: " - + isInstallsFromUnknownSourcesPermitted() - + "\n\tinstallRequestFromUnknownSource: " + isInstallRequestFromUnknownSource() - + "\n\tverifyAppsEnabled: " + isVerifyAppsEnabled() - + "\n\tappVerifierInstalled: " + isAppVerifierInstalled() - + "\n\tfileUri: " + isFileUri() - + "\n\treplace: " + isReplace() - + "\n\tsystemApp: " + isSystemApp() - + "\n\tpackageInfoObtained: " + isPackageInfoObtained() - + "\n\tinstallButtonClicked: " + isInstallButtonClicked() - + "\n\tpermissionsDisplayed: " + isPermissionsDisplayed() - + "\n\tnewPermissionsDisplayed: " + isNewPermissionsDisplayed() - + "\n\tallPermissionsDisplayed: " + isAllPermissionsDisplayed() - + "\n\tnewPermissionsFound: " + isNewPermissionsFound() - + "\n\tresult: " + mResult - + "\n\tpackageManagerInstallResult: " + mPackageManagerInstallResult - + "\n\ttotalDuration: " + (mEndTimestampMillis - mStartTimestampMillis) + " ms" - + "\n\ttimeTillPackageInfoObtained: " - + ((isPackageInfoObtained()) - ? ((mPackageInfoObtainedTimestampMillis - mStartTimestampMillis) - + " ms") - : "n/a") - + "\n\ttimeTillInstallButtonClick: " - + ((isInstallButtonClicked()) - ? ((mInstallButtonClickTimestampMillis - mStartTimestampMillis) + " ms") - : "n/a")); - Log.v(TAG, "Wrote to Event Log: 0x" + Long.toString(resultAndFlags & 0xffffffffL, 16) - + ", " + totalElapsedTime - + ", " + elapsedTimeTillPackageInfoObtained - + ", " + elapsedTimeTillInstallButtonClick); - } - } - - private static final byte clipUnsignedValueToUnsignedByte(long value) { - if (value < 0) { - return 0; - } else if (value > 0xff) { - return (byte) 0xff; - } else { - return (byte) value; - } - } - - private static final int clipUnsignedLongToUnsignedInt(long value) { - if (value < 0) { - return 0; - } else if (value > 0xffffffffL) { - return 0xffffffff; - } else { - return (int) value; - } - } - - /** - * Sets or clears the specified flag in the {@link #mFlags} field. - */ - private void setFlagState(int flag, boolean set) { - if (set) { - mFlags |= flag; - } else { - mFlags &= ~flag; - } - } - - /** - * Checks whether the specified flag is set in the {@link #mFlags} field. - */ - private boolean isFlagSet(int flag) { - return (mFlags & flag) == flag; - } - - /** - * Checks whether the user has consented to app verification. - */ - private boolean isUserConsentToVerifyAppsGranted() { - return Settings.Secure.getInt( - mContext.getContentResolver(), - Settings.Secure.PACKAGE_VERIFIER_USER_CONSENT, 0) != 0; - } - - /** - * Gets the digest of the contents of the package being installed. - */ - private byte[] getPackageContentsDigest() throws IOException { - File file = new File(Uri.parse(mPackageUri).getPath()); - return getSha256ContentsDigest(file); - } - - /** - * Gets the SHA-256 digest of the contents of the specified file. - */ - private static byte[] getSha256ContentsDigest(File file) throws IOException { - MessageDigest digest; - try { - digest = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("SHA-256 not available", e); - } - - byte[] buf = new byte[8192]; - InputStream in = null; - try { - in = new BufferedInputStream(new FileInputStream(file), buf.length); - int chunkSize; - while ((chunkSize = in.read(buf)) != -1) { - digest.update(buf, 0, chunkSize); - } - } finally { - IoUtils.closeQuietly(in); - } - return digest.digest(); - } -} diff --git a/src/com/android/packageinstaller/PackageInstallerActivity.java b/src/com/android/packageinstaller/PackageInstallerActivity.java index 868872a9..1903f917 100644 --- a/src/com/android/packageinstaller/PackageInstallerActivity.java +++ b/src/com/android/packageinstaller/PackageInstallerActivity.java @@ -24,21 +24,19 @@ import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.Intent; -import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; -import android.content.pm.ManifestDigest; import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageParser; import android.content.pm.PackageUserState; -import android.content.pm.ResolveInfo; import android.content.pm.VerificationParams; +import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; -import android.os.SystemClock; import android.os.UserManager; import android.provider.Settings; import android.support.v4.view.ViewPager; @@ -49,11 +47,16 @@ import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.AppSecurityPermissions; import android.widget.Button; +import android.widget.ImageView; import android.widget.TabHost; import android.widget.TextView; +import com.android.packageinstaller.permission.utils.Utils; import java.io.File; -import java.util.List; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; /* * This activity is launched when a new application is installed via side loading @@ -68,12 +71,20 @@ import java.util.List; public class PackageInstallerActivity extends Activity implements OnCancelListener, OnClickListener { private static final String TAG = "PackageInstaller"; + private static final int REQUEST_ENABLE_UNKNOWN_SOURCES = 1; + + private static final String SCHEME_FILE = "file"; + private static final String SCHEME_CONTENT = "content"; + private static final String SCHEME_PACKAGE = "package"; + private int mSessionId = -1; private Uri mPackageURI; private Uri mOriginatingURI; private Uri mReferrerURI; private int mOriginatingUid = VerificationParams.NO_UID; - private ManifestDigest mPkgDigest; + private File mContentUriApkStagingFile; + + private AsyncTask<Uri, Void, File> mStagingAsynTask; private boolean localLOGV = false; PackageManager mPm; @@ -85,8 +96,6 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen // ApplicationInfo object primarily used for already existing applications private ApplicationInfo mAppInfo = null; - private InstallFlowAnalytics mInstallFlowAnalytics; - // View for install progress View mInstallConfirm; // Buttons to indicate user acceptance @@ -97,8 +106,6 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen static final String PREFS_ALLOWED_SOURCES = "allowed_sources"; - private static final String PACKAGE_MIME_TYPE = "application/vnd.android.package-archive"; - private static final String TAB_ID_ALL = "all"; private static final String TAB_ID_NEW = "new"; @@ -108,25 +115,15 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen private static final int DLG_PACKAGE_ERROR = DLG_BASE + 2; private static final int DLG_OUT_OF_SPACE = DLG_BASE + 3; private static final int DLG_INSTALL_ERROR = DLG_BASE + 4; - private static final int DLG_ALLOW_SOURCE = DLG_BASE + 5; private static final int DLG_ADMIN_RESTRICTS_UNKNOWN_SOURCES = DLG_BASE + 6; private static final int DLG_NOT_SUPPORTED_ON_WEAR = DLG_BASE + 7; private void startInstallConfirm() { TabHost tabHost = (TabHost)findViewById(android.R.id.tabhost); tabHost.setup(); + tabHost.setVisibility(View.VISIBLE); ViewPager viewPager = (ViewPager)findViewById(R.id.pager); TabsAdapter adapter = new TabsAdapter(this, tabHost, viewPager); - adapter.setOnTabChangedListener(new TabHost.OnTabChangeListener() { - @Override - public void onTabChanged(String tabId) { - if (TAB_ID_ALL.equals(tabId)) { - mInstallFlowAnalytics.setAllPermissionsDisplayed(true); - } else if (TAB_ID_NEW.equals(tabId)) { - mInstallFlowAnalytics.setNewPermissionsDisplayed(true); - } - } - }); // If the app supports runtime permissions the new permissions will // be requested at runtime, hence we do not show them at install. boolean supportsRuntimePermissions = mPkgInfo.applicationInfo.targetSdkVersion @@ -148,7 +145,6 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen if (!supportsRuntimePermissions) { newPermissionsFound = (perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW) > 0); - mInstallFlowAnalytics.setNewPermissionsFound(newPermissionsFound); if (newPermissionsFound) { permVisible = true; mScrollView.addView(perms.getPermissionsView( @@ -166,7 +162,7 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen getText(R.string.newPerms)), mScrollView); } else { findViewById(R.id.tabscontainer).setVisibility(View.GONE); - findViewById(R.id.divider).setVisibility(View.VISIBLE); + findViewById(R.id.spacer).setVisibility(View.VISIBLE); } if (!supportsRuntimePermissions && N > 0) { permVisible = true; @@ -181,7 +177,6 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen adapter.addTab(tabHost.newTabSpec(TAB_ID_ALL).setIndicator( getText(R.string.allPerms)), root); } - mInstallFlowAnalytics.setPermissionsDisplayed(permVisible); if (!permVisible) { if (mAppInfo != null) { // This is an update to an application, but there are no @@ -189,25 +184,20 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen msg = (mAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 ? R.string.install_confirm_question_update_system_no_perms : R.string.install_confirm_question_update_no_perms; + + findViewById(R.id.spacer).setVisibility(View.VISIBLE); } else { // This is a new application with no permissions. msg = R.string.install_confirm_question_no_perms; } - tabHost.setVisibility(View.GONE); - mInstallFlowAnalytics.setAllPermissionsDisplayed(false); - mInstallFlowAnalytics.setNewPermissionsDisplayed(false); - findViewById(R.id.filler).setVisibility(View.VISIBLE); - findViewById(R.id.divider).setVisibility(View.GONE); + tabHost.setVisibility(View.INVISIBLE); mScrollView = null; } if (msg != 0) { ((TextView)findViewById(R.id.install_confirm_question)).setText(msg); } mInstallConfirm.setVisibility(View.VISIBLE); - mOk = (Button)findViewById(R.id.ok_button); - mCancel = (Button)findViewById(R.id.cancel_button); - mOk.setOnClickListener(this); - mCancel.setOnClickListener(this); + mOk.setEnabled(true); if (mScrollView == null) { // There is nothing to scroll view, so the ok button is immediately // set to install. @@ -235,24 +225,22 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen switch (id) { case DLG_UNKNOWN_SOURCES: return new AlertDialog.Builder(this) - .setTitle(R.string.unknown_apps_dlg_title) .setMessage(R.string.unknown_apps_dlg_text) .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { Log.i(TAG, "Finishing off activity so that user can navigate to settings manually"); - finish(); + finishAffinity(); }}) .setPositiveButton(R.string.settings, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { Log.i(TAG, "Launching settings"); - launchSettingsAppAndFinish(); + launchSecuritySettings(); } }) .setOnCancelListener(this) .create(); case DLG_ADMIN_RESTRICTS_UNKNOWN_SOURCES: return new AlertDialog.Builder(this) - .setTitle(R.string.unknown_apps_dlg_title) .setMessage(R.string.unknown_apps_admin_dlg_text) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { @@ -263,7 +251,6 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen .create(); case DLG_PACKAGE_ERROR : return new AlertDialog.Builder(this) - .setTitle(R.string.Parse_error_dlg_title) .setMessage(R.string.Parse_error_dlg_text) .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { @@ -278,7 +265,6 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen String dlgText = getString(R.string.out_of_space_dlg_text, appTitle.toString()); return new AlertDialog.Builder(this) - .setTitle(R.string.out_of_space_dlg_title) .setMessage(dlgText) .setPositiveButton(R.string.manage_applications, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { @@ -303,7 +289,6 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen String dlgText1 = getString(R.string.install_failed_msg, appTitle1.toString()); return new AlertDialog.Builder(this) - .setTitle(R.string.install_failed) .setNeutralButton(R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { finish(); @@ -312,50 +297,36 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen .setMessage(dlgText1) .setOnCancelListener(this) .create(); - case DLG_ALLOW_SOURCE: - CharSequence appTitle2 = mPm.getApplicationLabel(mSourceInfo); - String dlgText2 = getString(R.string.allow_source_dlg_text, - appTitle2.toString()); - return new AlertDialog.Builder(this) - .setTitle(R.string.allow_source_dlg_title) - .setMessage(dlgText2) - .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - setResult(RESULT_CANCELED); - finish(); - }}) - .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - SharedPreferences prefs = getSharedPreferences(PREFS_ALLOWED_SOURCES, - Context.MODE_PRIVATE); - prefs.edit().putBoolean(mSourceInfo.packageName, true).apply(); - startInstallConfirm(); - } - }) - .setOnCancelListener(this) - .create(); case DLG_NOT_SUPPORTED_ON_WEAR: return new AlertDialog.Builder(this) - .setTitle(R.string.wear_not_allowed_dlg_title) .setMessage(R.string.wear_not_allowed_dlg_text) .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { setResult(RESULT_OK); - finish(); + clearCachedApkIfNeededAndFinish(); } }) .setOnCancelListener(this) .create(); } return null; - } + } - private void launchSettingsAppAndFinish() { - // Create an intent to launch SettingsTwo activity + private void launchSecuritySettings() { Intent launchSettingsIntent = new Intent(Settings.ACTION_SECURITY_SETTINGS); - launchSettingsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(launchSettingsIntent); - finish(); + startActivityForResult(launchSettingsIntent, REQUEST_ENABLE_UNKNOWN_SOURCES); + } + + @Override + public void onActivityResult(int request, int result, Intent data) { + // If the settings app approved the install we are good to go regardless + // whether the untrusted sources setting is on. This allows partners to + // implement a "allow untrusted source once" feature. + if (request == REQUEST_ENABLE_UNKNOWN_SOURCES && result == RESULT_OK) { + initiateInstall(); + } else { + clearCachedApkIfNeededAndFinish(); + } } private boolean isInstallRequestFromUnknownSource(Intent intent) { @@ -378,23 +349,6 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen return true; } - private boolean isVerifyAppsEnabled() { - if (mUserManager.hasUserRestriction(UserManager.ENSURE_VERIFY_APPS)) { - return true; - } - return Settings.Global.getInt(getContentResolver(), - Settings.Global.PACKAGE_VERIFIER_ENABLE, 1) > 0; - } - - private boolean isAppVerifierInstalled() { - final PackageManager pm = getPackageManager(); - final Intent verification = new Intent(Intent.ACTION_PACKAGE_NEEDS_VERIFICATION); - verification.setType(PACKAGE_MIME_TYPE); - verification.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - final List<ResolveInfo> receivers = pm.queryBroadcastReceivers(verification, 0); - return (receivers.size() > 0) ? true : false; - } - /** * @return whether unknown sources is enabled by user in Settings */ @@ -434,10 +388,6 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen mAppInfo = null; } - mInstallFlowAnalytics.setReplace(mAppInfo != null); - mInstallFlowAnalytics.setSystemApp( - (mAppInfo != null) && ((mAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0)); - startInstallConfirm(); } @@ -457,6 +407,10 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen mUserManager = (UserManager) getSystemService(Context.USER_SERVICE); final Intent intent = getIntent(); + mOriginatingUid = getOriginatingUid(intent); + + final Uri packageUri; + if (PackageInstaller.ACTION_CONFIRM_PERMISSIONS.equals(intent.getAction())) { final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1); final PackageInstaller.SessionInfo info = mInstaller.getSessionInfo(sessionId); @@ -467,120 +421,130 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen } mSessionId = sessionId; - mPackageURI = Uri.fromFile(new File(info.resolvedBaseCodePath)); + packageUri = Uri.fromFile(new File(info.resolvedBaseCodePath)); mOriginatingURI = null; mReferrerURI = null; } else { mSessionId = -1; - mPackageURI = intent.getData(); + packageUri = intent.getData(); mOriginatingURI = intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI); mReferrerURI = intent.getParcelableExtra(Intent.EXTRA_REFERRER); } - final boolean unknownSourcesAllowedByAdmin = isUnknownSourcesAllowedByAdmin(); - final boolean unknownSourcesAllowedByUser = isUnknownSourcesEnabled(); - - boolean requestFromUnknownSource = isInstallRequestFromUnknownSource(intent); - mInstallFlowAnalytics = new InstallFlowAnalytics(); - mInstallFlowAnalytics.setContext(this); - mInstallFlowAnalytics.setStartTimestampMillis(SystemClock.elapsedRealtime()); - mInstallFlowAnalytics.setInstallsFromUnknownSourcesPermitted(unknownSourcesAllowedByAdmin - && unknownSourcesAllowedByUser); - mInstallFlowAnalytics.setInstallRequestFromUnknownSource(requestFromUnknownSource); - mInstallFlowAnalytics.setVerifyAppsEnabled(isVerifyAppsEnabled()); - mInstallFlowAnalytics.setAppVerifierInstalled(isAppVerifierInstalled()); - mInstallFlowAnalytics.setPackageUri(mPackageURI.toString()); - - if (DeviceUtils.isWear(this)) { - showDialogInner(DLG_NOT_SUPPORTED_ON_WEAR); - mInstallFlowAnalytics.setFlowFinished( - InstallFlowAnalytics.RESULT_NOT_ALLOWED_ON_WEAR); - return; - } - - final String scheme = mPackageURI.getScheme(); - if (scheme != null && !"file".equals(scheme) && !"package".equals(scheme)) { - Log.w(TAG, "Unsupported scheme " + scheme); + // if there's nothing to do, quietly slip into the ether + if (packageUri == null) { + Log.w(TAG, "Unspecified source"); setPmResult(PackageManager.INSTALL_FAILED_INVALID_URI); - mInstallFlowAnalytics.setFlowFinished( - InstallFlowAnalytics.RESULT_FAILED_UNSUPPORTED_SCHEME); finish(); return; } - final PackageUtil.AppSnippet as; - if ("package".equals(mPackageURI.getScheme())) { - mInstallFlowAnalytics.setFileUri(false); - try { - mPkgInfo = mPm.getPackageInfo(mPackageURI.getSchemeSpecificPart(), - PackageManager.GET_PERMISSIONS | PackageManager.GET_UNINSTALLED_PACKAGES); - } catch (NameNotFoundException e) { - } - if (mPkgInfo == null) { - Log.w(TAG, "Requested package " + mPackageURI.getScheme() - + " not available. Discontinuing installation"); - showDialogInner(DLG_PACKAGE_ERROR); - setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK); - mInstallFlowAnalytics.setPackageInfoObtained(); - mInstallFlowAnalytics.setFlowFinished( - InstallFlowAnalytics.RESULT_FAILED_PACKAGE_MISSING); - return; - } - as = new PackageUtil.AppSnippet(mPm.getApplicationLabel(mPkgInfo.applicationInfo), - mPm.getApplicationIcon(mPkgInfo.applicationInfo)); - } else { - mInstallFlowAnalytics.setFileUri(true); - final File sourceFile = new File(mPackageURI.getPath()); - PackageParser.Package parsed = PackageUtil.getPackageInfo(sourceFile); - - // Check for parse errors - if (parsed == null) { - Log.w(TAG, "Parse error when parsing manifest. Discontinuing installation"); - showDialogInner(DLG_PACKAGE_ERROR); - setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK); - mInstallFlowAnalytics.setPackageInfoObtained(); - mInstallFlowAnalytics.setFlowFinished( - InstallFlowAnalytics.RESULT_FAILED_TO_GET_PACKAGE_INFO); - return; - } - mPkgInfo = PackageParser.generatePackageInfo(parsed, null, - PackageManager.GET_PERMISSIONS, 0, 0, null, - new PackageUserState()); - mPkgDigest = parsed.manifestDigest; - as = PackageUtil.getAppSnippet(this, mPkgInfo.applicationInfo, sourceFile); + if (DeviceUtils.isWear(this)) { + showDialogInner(DLG_NOT_SUPPORTED_ON_WEAR); + return; } - mInstallFlowAnalytics.setPackageInfoObtained(); //set view setContentView(R.layout.install_start); mInstallConfirm = findViewById(R.id.install_confirm_panel); mInstallConfirm.setVisibility(View.INVISIBLE); - PackageUtil.initSnippetForNewApp(this, as, R.id.app_snippet); - - mOriginatingUid = getOriginatingUid(intent); + mOk = (Button)findViewById(R.id.ok_button); + mCancel = (Button)findViewById(R.id.cancel_button); + mOk.setOnClickListener(this); + mCancel.setOnClickListener(this); // Block the install attempt on the Unknown Sources setting if necessary. + final boolean requestFromUnknownSource = isInstallRequestFromUnknownSource(intent); if (!requestFromUnknownSource) { - initiateInstall(); + processPackageUri(packageUri); return; } // If the admin prohibits it, or we're running in a managed profile, just show error // and exit. Otherwise show an option to take the user to Settings to change the setting. final boolean isManagedProfile = mUserManager.isManagedProfile(); - if (!unknownSourcesAllowedByAdmin - || (!unknownSourcesAllowedByUser && isManagedProfile)) { + if (!isUnknownSourcesAllowedByAdmin()) { + startActivity(new Intent(Settings.ACTION_SHOW_ADMIN_SUPPORT_DETAILS)); + clearCachedApkIfNeededAndFinish(); + } else if (!isUnknownSourcesEnabled() && isManagedProfile) { showDialogInner(DLG_ADMIN_RESTRICTS_UNKNOWN_SOURCES); - mInstallFlowAnalytics.setFlowFinished( - InstallFlowAnalytics.RESULT_BLOCKED_BY_UNKNOWN_SOURCES_SETTING); - } else if (!unknownSourcesAllowedByUser) { + } else if (!isUnknownSourcesEnabled()) { // Ask user to enable setting first + showDialogInner(DLG_UNKNOWN_SOURCES); - mInstallFlowAnalytics.setFlowFinished( - InstallFlowAnalytics.RESULT_BLOCKED_BY_UNKNOWN_SOURCES_SETTING); } else { - initiateInstall(); + processPackageUri(packageUri); + } + } + + @Override + protected void onDestroy() { + if (mStagingAsynTask != null) { + mStagingAsynTask.cancel(true); + mStagingAsynTask = null; } + super.onDestroy(); + } + + private void processPackageUri(final Uri packageUri) { + mPackageURI = packageUri; + + final String scheme = packageUri.getScheme(); + final PackageUtil.AppSnippet as; + + switch (scheme) { + case SCHEME_PACKAGE: { + try { + mPkgInfo = mPm.getPackageInfo(packageUri.getSchemeSpecificPart(), + PackageManager.GET_PERMISSIONS + | PackageManager.GET_UNINSTALLED_PACKAGES); + } catch (NameNotFoundException e) { + } + if (mPkgInfo == null) { + Log.w(TAG, "Requested package " + packageUri.getScheme() + + " not available. Discontinuing installation"); + showDialogInner(DLG_PACKAGE_ERROR); + setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK); + return; + } + as = new PackageUtil.AppSnippet(mPm.getApplicationLabel(mPkgInfo.applicationInfo), + mPm.getApplicationIcon(mPkgInfo.applicationInfo)); + } break; + + case SCHEME_FILE: { + File sourceFile = new File(packageUri.getPath()); + PackageParser.Package parsed = PackageUtil.getPackageInfo(sourceFile); + + // Check for parse errors + if (parsed == null) { + Log.w(TAG, "Parse error when parsing manifest. Discontinuing installation"); + showDialogInner(DLG_PACKAGE_ERROR); + setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK); + return; + } + mPkgInfo = PackageParser.generatePackageInfo(parsed, null, + PackageManager.GET_PERMISSIONS, 0, 0, null, + new PackageUserState()); + as = PackageUtil.getAppSnippet(this, mPkgInfo.applicationInfo, sourceFile); + } break; + + case SCHEME_CONTENT: { + mStagingAsynTask = new StagingAsyncTask(); + mStagingAsynTask.execute(packageUri); + return; + } + + default: { + Log.w(TAG, "Unsupported scheme " + scheme); + setPmResult(PackageManager.INSTALL_FAILED_INVALID_URI); + clearCachedApkIfNeededAndFinish(); + return; + } + } + + PackageUtil.initSnippetForNewApp(this, as, R.id.app_snippet); + + initiateInstall(); } /** Get the ApplicationInfo for the calling package, if available */ @@ -660,43 +624,33 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen if (mSessionId != -1) { mInstaller.setPermissionsResult(mSessionId, false); } - mInstallFlowAnalytics.setFlowFinished( - InstallFlowAnalytics.RESULT_CANCELLED_BY_USER); super.onBackPressed(); } // Generic handling when pressing back key public void onCancel(DialogInterface dialog) { - finish(); + clearCachedApkIfNeededAndFinish(); } public void onClick(View v) { if (v == mOk) { if (mOkCanInstall || mScrollView == null) { - mInstallFlowAnalytics.setInstallButtonClicked(); if (mSessionId != -1) { mInstaller.setPermissionsResult(mSessionId, true); - - // We're only confirming permissions, so we don't really know how the - // story ends; assume success. - mInstallFlowAnalytics.setFlowFinishedWithPackageManagerResult( - PackageManager.INSTALL_SUCCEEDED); - finish(); + clearCachedApkIfNeededAndFinish(); } else { startInstall(); } } else { mScrollView.pageScroll(View.FOCUS_DOWN); } - } else if(v == mCancel) { + } else if (v == mCancel) { // Cancel and finish setResult(RESULT_CANCELED); if (mSessionId != -1) { mInstaller.setPermissionsResult(mSessionId, false); } - mInstallFlowAnalytics.setFlowFinished( - InstallFlowAnalytics.RESULT_CANCELLED_BY_USER); - finish(); + clearCachedApkIfNeededAndFinish(); } } @@ -707,9 +661,6 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen mPkgInfo.applicationInfo); newIntent.setData(mPackageURI); newIntent.setClass(this, InstallAppProgress.class); - newIntent.putExtra(InstallAppProgress.EXTRA_MANIFEST_DIGEST, mPkgDigest); - newIntent.putExtra( - InstallAppProgress.EXTRA_INSTALL_FLOW_ANALYTICS, mInstallFlowAnalytics); String installerPackageName = getIntent().getStringExtra( Intent.EXTRA_INSTALLER_PACKAGE_NAME); if (mOriginatingURI != null) { @@ -733,4 +684,98 @@ public class PackageInstallerActivity extends Activity implements OnCancelListen startActivity(newIntent); finish(); } + + private void clearCachedApkIfNeededAndFinish() { + if (mContentUriApkStagingFile != null) { + mContentUriApkStagingFile.delete(); + mContentUriApkStagingFile = null; + } + finish(); + } + + private final class StagingAsyncTask extends AsyncTask<Uri, Void, File> { + private static final long SHOW_EMPTY_STATE_DELAY_MILLIS = 300; + + private final Runnable mEmptyStateRunnable = new Runnable() { + @Override + public void run() { + ((TextView) findViewById(R.id.app_name)).setText(R.string.app_name_unknown); + ((TextView) findViewById(R.id.install_confirm_question)) + .setText(R.string.message_staging); + mInstallConfirm.setVisibility(View.VISIBLE); + findViewById(android.R.id.tabhost).setVisibility(View.INVISIBLE); + findViewById(R.id.spacer).setVisibility(View.VISIBLE); + findViewById(R.id.ok_button).setEnabled(false); + Drawable icon = getDrawable(R.drawable.ic_file_download); + Utils.applyTint(PackageInstallerActivity.this, + icon, android.R.attr.colorControlNormal); + ((ImageView) findViewById(R.id.app_icon)).setImageDrawable(icon); + } + }; + + @Override + protected void onPreExecute() { + getWindow().getDecorView().postDelayed(mEmptyStateRunnable, + SHOW_EMPTY_STATE_DELAY_MILLIS); + } + + @Override + protected File doInBackground(Uri... params) { + if (params == null || params.length <= 0) { + return null; + } + Uri packageUri = params[0]; + File sourceFile = null; + try { + sourceFile = File.createTempFile("package", ".apk", getCacheDir()); + try ( + InputStream in = getContentResolver().openInputStream(packageUri); + OutputStream out = (in != null) ? new FileOutputStream( + sourceFile) : null; + ) { + // Despite the comments in ContentResolver#openInputStream + // the returned stream can be null. + if (in == null) { + return null; + } + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = in.read(buffer)) >= 0) { + // Be nice and respond to a cancellation + if (isCancelled()) { + return null; + } + out.write(buffer, 0, bytesRead); + } + } + } catch (IOException ioe) { + Log.w(TAG, "Error staging apk from content URI", ioe); + if (sourceFile != null) { + sourceFile.delete(); + } + } + return sourceFile; + } + + @Override + protected void onPostExecute(File file) { + getWindow().getDecorView().removeCallbacks(mEmptyStateRunnable); + if (isFinishing() || isDestroyed()) { + return; + } + if (file == null) { + showDialogInner(DLG_PACKAGE_ERROR); + setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK); + return; + } + mContentUriApkStagingFile = file; + Uri fileUri = Uri.fromFile(file); + processPackageUri(fileUri); + } + + @Override + protected void onCancelled(File file) { + getWindow().getDecorView().removeCallbacks(mEmptyStateRunnable); + } + }; } diff --git a/src/com/android/packageinstaller/PackageUtil.java b/src/com/android/packageinstaller/PackageUtil.java index 37e96f0a..330cbbce 100644 --- a/src/com/android/packageinstaller/PackageUtil.java +++ b/src/com/android/packageinstaller/PackageUtil.java @@ -47,27 +47,12 @@ public class PackageUtil { public static final String INTENT_ATTR_PACKAGE_NAME=PREFIX+"PackageName"; /** - * Utility method to get application information for a given {@link File} - */ - public static ApplicationInfo getApplicationInfo(File sourcePath) { - final PackageParser parser = new PackageParser(); - try { - PackageParser.Package pkg = parser.parseMonolithicPackage(sourcePath, 0); - return pkg.applicationInfo; - } catch (PackageParserException e) { - return null; - } - } - - /** * Utility method to get package information for a given {@link File} */ public static PackageParser.Package getPackageInfo(File sourceFile) { final PackageParser parser = new PackageParser(); try { - PackageParser.Package pkg = parser.parseMonolithicPackage(sourceFile, 0); - parser.collectManifestDigest(pkg); - return pkg; + return parser.parsePackage(sourceFile, 0); } catch (PackageParserException e) { return null; } diff --git a/src/com/android/packageinstaller/TabsAdapter.java b/src/com/android/packageinstaller/TabsAdapter.java index 699cbed3..3509e092 100644 --- a/src/com/android/packageinstaller/TabsAdapter.java +++ b/src/com/android/packageinstaller/TabsAdapter.java @@ -46,7 +46,6 @@ public class TabsAdapter extends PagerAdapter private final ViewPager mViewPager; private final ArrayList<TabInfo> mTabs = new ArrayList<TabInfo>(); private final Rect mTempRect = new Rect(); - private TabHost.OnTabChangeListener mOnTabChangeListener; static final class TabInfo { private final String tag; @@ -115,17 +114,10 @@ public class TabsAdapter extends PagerAdapter return view == object; } - public void setOnTabChangedListener(TabHost.OnTabChangeListener listener) { - mOnTabChangeListener = listener; - } - @Override public void onTabChanged(String tabId) { int position = mTabHost.getCurrentTab(); mViewPager.setCurrentItem(position); - if (mOnTabChangeListener != null) { - mOnTabChangeListener.onTabChanged(tabId); - } } @Override diff --git a/src/com/android/packageinstaller/UninstallAppProgress.java b/src/com/android/packageinstaller/UninstallAppProgress.java index d6b788d4..63a243b2 100755 --- a/src/com/android/packageinstaller/UninstallAppProgress.java +++ b/src/com/android/packageinstaller/UninstallAppProgress.java @@ -27,6 +27,9 @@ import android.content.pm.IPackageManager; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.pm.UserInfo; +import android.content.res.Configuration; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -35,7 +38,9 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; +import android.provider.Settings; import android.util.Log; +import android.util.TypedValue; import android.view.KeyEvent; import android.view.View; import android.view.View.OnClickListener; @@ -61,14 +66,22 @@ public class UninstallAppProgress extends Activity implements OnClickListener { private UserHandle mUser; private IBinder mCallback; - private TextView mStatusTextView; private Button mOkButton; private Button mDeviceManagerButton; - private ProgressBar mProgressBar; - private View mOkPanel; + private Button mUsersButton; private volatile int mResultCode = -1; + /** + * If initView was called. We delay this call to not have to call it at all if the uninstall is + * quick + */ + private boolean mIsViewInitialized; + + /** Amount of time to wait until we show the UI */ + private static final int QUICK_INSTALL_DELAY_MILLIS = 500; + private static final int UNINSTALL_COMPLETE = 1; + private static final int UNINSTALL_IS_SLOW = 2; private boolean isProfileOfOrSame(UserManager userManager, int userId, int profileId) { if (userId == profileId) { @@ -80,8 +93,21 @@ public class UninstallAppProgress extends Activity implements OnClickListener { private Handler mHandler = new Handler() { public void handleMessage(Message msg) { + if (isFinishing() || isDestroyed()) { + return; + } + switch (msg.what) { + case UNINSTALL_IS_SLOW: + initView(); + break; case UNINSTALL_COMPLETE: + mHandler.removeMessages(UNINSTALL_IS_SLOW); + + if (msg.arg1 != PackageManager.DELETE_SUCCEEDED) { + initView(); + } + mResultCode = msg.arg1; final String packageName = (String) msg.obj; @@ -180,15 +206,19 @@ public class UninstallAppProgress extends Activity implements OnClickListener { mDeviceManagerButton.setVisibility(View.VISIBLE); } else { mDeviceManagerButton.setVisibility(View.GONE); + mUsersButton.setVisibility(View.VISIBLE); } - if (blockingUserId == UserHandle.USER_OWNER) { + // TODO: b/25442806 + if (blockingUserId == UserHandle.USER_SYSTEM) { statusText = getString(R.string.uninstall_blocked_device_owner); } else if (blockingUserId == UserHandle.USER_NULL) { Log.d(TAG, "Uninstall failed for " + packageName + " with code " + msg.arg1 + " no blocking user"); statusText = getString(R.string.uninstall_failed); } else { - statusText = getString(R.string.uninstall_blocked_profile_owner); + statusText = mAllUsers + ? getString(R.string.uninstall_all_blocked_profile_owner) : + getString(R.string.uninstall_blocked_profile_owner); } break; } @@ -198,11 +228,10 @@ public class UninstallAppProgress extends Activity implements OnClickListener { statusText = getString(R.string.uninstall_failed); break; } - mStatusTextView.setText(statusText); - - // Hide the progress bar; Show the ok button - mProgressBar.setVisibility(View.INVISIBLE); - mOkPanel.setVisibility(View.VISIBLE); + findViewById(R.id.progress_view).setVisibility(View.GONE); + findViewById(R.id.status_view).setVisibility(View.VISIBLE); + ((TextView)findViewById(R.id.status_text)).setText(statusText); + findViewById(R.id.ok_panel).setVisibility(View.VISIBLE); break; default: break; @@ -213,11 +242,34 @@ public class UninstallAppProgress extends Activity implements OnClickListener { @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); + Intent intent = getIntent(); mAppInfo = intent.getParcelableExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO); + mCallback = intent.getIBinderExtra(PackageInstaller.EXTRA_CALLBACK); + + // This currently does not support going through a onDestroy->onCreate cycle. Hence if that + // happened, just fail the operation for mysterious reasons. + if (icicle != null) { + mResultCode = PackageManager.DELETE_FAILED_INTERNAL_ERROR; + + if (mCallback != null) { + final IPackageDeleteObserver2 observer = IPackageDeleteObserver2.Stub + .asInterface(mCallback); + try { + observer.onPackageDeleted(mAppInfo.packageName, mResultCode, null); + } catch (RemoteException ignored) { + } + finish(); + } else { + setResultAndFinish(mResultCode); + } + + return; + } + mAllUsers = intent.getBooleanExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, false); - if (mAllUsers && UserHandle.myUserId() != UserHandle.USER_OWNER) { - throw new SecurityException("Only owner user can request uninstall for all users"); + if (mAllUsers && !UserManager.get(this).isAdminUser()) { + throw new SecurityException("Only admin user can request uninstall for all users"); } mUser = intent.getParcelableExtra(Intent.EXTRA_USER); if (mUser == null) { @@ -230,8 +282,21 @@ public class UninstallAppProgress extends Activity implements OnClickListener { + "request uninstall for user " + mUser); } } - mCallback = intent.getIBinderExtra(PackageInstaller.EXTRA_CALLBACK); - initView(); + + PackageDeleteObserver observer = new PackageDeleteObserver(); + + // Make window transparent until initView is called. In many cases we can avoid showing the + // UI at all as the app is uninstalled very quickly. If we show the UI and instantly remove + // it, it just looks like a flicker. + getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + getWindow().setStatusBarColor(Color.TRANSPARENT); + getWindow().setNavigationBarColor(Color.TRANSPARENT); + + getPackageManager().deletePackageAsUser(mAppInfo.packageName, observer, + mAllUsers ? PackageManager.DELETE_ALL_USERS : 0, mUser.getIdentifier()); + + mHandler.sendMessageDelayed(mHandler.obtainMessage(UNINSTALL_IS_SLOW), + QUICK_INSTALL_DELAY_MILLIS); } class PackageDeleteObserver extends IPackageDeleteObserver.Stub { @@ -249,6 +314,28 @@ public class UninstallAppProgress extends Activity implements OnClickListener { } public void initView() { + if (mIsViewInitialized) { + return; + } + mIsViewInitialized = true; + + // We set the window background to translucent in constructor, revert this + TypedValue attribute = new TypedValue(); + getTheme().resolveAttribute(android.R.attr.windowBackground, attribute, true); + if (attribute.type >= TypedValue.TYPE_FIRST_COLOR_INT && + attribute.type <= TypedValue.TYPE_LAST_COLOR_INT) { + getWindow().setBackgroundDrawable(new ColorDrawable(attribute.data)); + } else { + getWindow().setBackgroundDrawable(getResources().getDrawable(attribute.resourceId, + getTheme())); + } + + getTheme().resolveAttribute(android.R.attr.navigationBarColor, attribute, true); + getWindow().setNavigationBarColor(attribute.data); + + getTheme().resolveAttribute(android.R.attr.statusBarColor, attribute, true); + getWindow().setStatusBarColor(attribute.data); + boolean isUpdate = ((mAppInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0); setTitle(isUpdate ? R.string.uninstall_update_title : R.string.uninstall_application_title); @@ -256,9 +343,8 @@ public class UninstallAppProgress extends Activity implements OnClickListener { // Initialize views View snippetView = findViewById(R.id.app_snippet); PackageUtil.initSnippetForInstalledApp(this, mAppInfo, snippetView); - mStatusTextView = (TextView) findViewById(R.id.center_text); - mStatusTextView.setText(R.string.uninstalling); mDeviceManagerButton = (Button) findViewById(R.id.device_manager_button); + mUsersButton = (Button) findViewById(R.id.users_button); mDeviceManagerButton.setVisibility(View.GONE); mDeviceManagerButton.setOnClickListener(new OnClickListener() { @Override @@ -271,24 +357,19 @@ public class UninstallAppProgress extends Activity implements OnClickListener { finish(); } }); - mProgressBar = (ProgressBar) findViewById(R.id.progress_bar); - mProgressBar.setIndeterminate(true); + mUsersButton.setVisibility(View.GONE); + mUsersButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(Settings.ACTION_USER_SETTINGS); + intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + finish(); + } + }); // Hide button till progress is being displayed - mOkPanel = (View) findViewById(R.id.ok_panel); mOkButton = (Button) findViewById(R.id.ok_button); mOkButton.setOnClickListener(this); - mOkPanel.setVisibility(View.INVISIBLE); - IPackageManager packageManager = - IPackageManager.Stub.asInterface(ServiceManager.getService("package")); - PackageDeleteObserver observer = new PackageDeleteObserver(); - try { - packageManager.deletePackageAsUser(mAppInfo.packageName, observer, - mUser.getIdentifier(), - mAllUsers ? PackageManager.DELETE_ALL_USERS : 0); - } catch (RemoteException e) { - // Shouldn't happen. - Log.e(TAG, "Failed to talk to package manager", e); - } } public void onClick(View v) { diff --git a/src/com/android/packageinstaller/UninstallerActivity.java b/src/com/android/packageinstaller/UninstallerActivity.java index e277d48f..ae1659f4 100755 --- a/src/com/android/packageinstaller/UninstallerActivity.java +++ b/src/com/android/packageinstaller/UninstallerActivity.java @@ -74,11 +74,15 @@ public class UninstallerActivity extends Activity { final boolean isUpdate = ((dialogInfo.appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0); + UserManager userManager = UserManager.get(getActivity()); if (isUpdate) { - messageBuilder.append(getString(R.string.uninstall_update_text)); + if (isSingleUser(userManager)) { + messageBuilder.append(getString(R.string.uninstall_update_text)); + } else { + messageBuilder.append(getString(R.string.uninstall_update_text_multiuser)); + } } else { - UserManager userManager = UserManager.get(getActivity()); - if (dialogInfo.allUsers && userManager.getUserCount() >= 2) { + if (dialogInfo.allUsers && !isSingleUser(userManager)) { messageBuilder.append(getString(R.string.uninstall_application_text_all_users)); } else if (!dialogInfo.user.equals(android.os.Process.myUserHandle())) { UserInfo userInfo = userManager.getUserInfo(dialogInfo.user.getIdentifier()); @@ -109,7 +113,19 @@ public class UninstallerActivity extends Activity { @Override public void onDismiss(DialogInterface dialog) { super.onDismiss(dialog); - getActivity().finish(); + if (isAdded()) { + getActivity().finish(); + } + } + + /** + * Returns whether there is only one user on this device, not including + * the system-only user. + */ + private boolean isSingleUser(UserManager userManager) { + final int userCount = userManager.getUserCount(); + return userCount == 1 + || (UserManager.isSplitSystemUser() && userCount == 2); } } @@ -127,9 +143,11 @@ public class UninstallerActivity extends Activity { @Override public void onDismiss(DialogInterface dialog) { super.onDismiss(dialog); - ((UninstallerActivity) getActivity()).dispatchAborted(); - getActivity().setResult(Activity.RESULT_FIRST_USER); - getActivity().finish(); + if (isAdded()) { + ((UninstallerActivity) getActivity()).dispatchAborted(); + getActivity().setResult(Activity.RESULT_FIRST_USER); + getActivity().finish(); + } } } diff --git a/src/com/android/packageinstaller/permission/model/AppPermissionGroup.java b/src/com/android/packageinstaller/permission/model/AppPermissionGroup.java index e54b7029..a6601165 100644 --- a/src/com/android/packageinstaller/permission/model/AppPermissionGroup.java +++ b/src/com/android/packageinstaller/permission/model/AppPermissionGroup.java @@ -25,10 +25,12 @@ import android.content.pm.PackageManager; import android.content.pm.PermissionGroupInfo; import android.content.pm.PermissionInfo; import android.os.Build; +import android.os.Process; import android.os.UserHandle; import android.util.ArrayMap; import com.android.packageinstaller.R; +import com.android.packageinstaller.permission.utils.ArrayUtils; import com.android.packageinstaller.permission.utils.LocationUtils; import java.util.ArrayList; @@ -67,7 +69,7 @@ public final class AppPermissionGroup implements Comparable<AppPermissionGroup> if (permissionInfo.protectionLevel != PermissionInfo.PROTECTION_DANGEROUS || (permissionInfo.flags & PermissionInfo.FLAG_INSTALLED) == 0 - || (permissionInfo.flags & PermissionInfo.FLAG_HIDDEN) != 0) { + || (permissionInfo.flags & PermissionInfo.FLAG_REMOVED) != 0) { return null; } @@ -92,7 +94,7 @@ public final class AppPermissionGroup implements Comparable<AppPermissionGroup> } return create(context, packageInfo, groupInfo, permissionInfos, - new UserHandle(context.getUserId())); + Process.myUserHandle()); } public static AppPermissionGroup create(Context context, PackageInfo packageInfo, @@ -135,21 +137,20 @@ public final class AppPermissionGroup implements Comparable<AppPermissionGroup> continue; } - // Don't allow toggle of non platform defined permissions for legacy apps via app ops. + // Don't allow toggling non-platform permission groups for legacy apps via app ops. if (packageInfo.applicationInfo.targetSdkVersion <= Build.VERSION_CODES.LOLLIPOP_MR1 - && !PLATFORM_PACKAGE_NAME.equals(requestedPermissionInfo.packageName)) { + && !PLATFORM_PACKAGE_NAME.equals(groupInfo.packageName)) { continue; } final boolean granted = (packageInfo.requestedPermissionsFlags[i] & PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0; - final int appOp = PLATFORM_PACKAGE_NAME.equals(requestedPermissionInfo.packageName) - ? AppOpsManager.permissionToOpCode(requestedPermissionInfo.name) - : AppOpsManager.OP_NONE; + final String appOp = PLATFORM_PACKAGE_NAME.equals(requestedPermissionInfo.packageName) + ? AppOpsManager.permissionToOp(requestedPermissionInfo.name) : null; - final boolean appOpAllowed = appOp != AppOpsManager.OP_NONE - && context.getSystemService(AppOpsManager.class).checkOp(appOp, + final boolean appOpAllowed = appOp != null + && context.getSystemService(AppOpsManager.class).checkOpNoThrow(appOp, packageInfo.applicationInfo.uid, packageInfo.packageName) == AppOpsManager.MODE_ALLOWED; @@ -209,23 +210,39 @@ public final class AppPermissionGroup implements Comparable<AppPermissionGroup> return mAppSupportsRuntimePermissions; } - - public boolean hasGrantedByDefaultPermission() { + public boolean isReviewRequired() { + if (mAppSupportsRuntimePermissions) { + return false; + } final int permissionCount = mPermissions.size(); for (int i = 0; i < permissionCount; i++) { Permission permission = mPermissions.valueAt(i); - if (permission.isGrantedByDefault()) { + if (permission.isReviewRequired()) { return true; } } return false; } - public boolean hasAppOpPermission() { + public void resetReviewRequired() { + final int permissionCount = mPermissions.size(); + for (int i = 0; i < permissionCount; i++) { + Permission permission = mPermissions.valueAt(i); + if (permission.isReviewRequired()) { + permission.resetReviewRequired(); + mPackageManager.updatePermissionFlags(permission.getName(), + mPackageInfo.packageName, + PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED, + 0, mUserHandle); + } + } + } + + public boolean hasGrantedByDefaultPermission() { final int permissionCount = mPermissions.size(); for (int i = 0; i < permissionCount; i++) { Permission permission = mPermissions.valueAt(i); - if (permission.getAppOp() != AppOpsManager.OP_NONE) { + if (permission.isGrantedByDefault()) { return true; } } @@ -260,24 +277,35 @@ public final class AppPermissionGroup implements Comparable<AppPermissionGroup> return mDescription; } + public int getUserId() { + return mUserHandle.getIdentifier(); + } + public boolean hasPermission(String permission) { return mPermissions.get(permission) != null; } public boolean areRuntimePermissionsGranted() { + return areRuntimePermissionsGranted(null); + } + + public boolean areRuntimePermissionsGranted(String[] filterPermissions) { if (LocationUtils.isLocationGroupAndProvider(mName, mPackageInfo.packageName)) { return LocationUtils.isLocationEnabled(mContext); } final int permissionCount = mPermissions.size(); for (int i = 0; i < permissionCount; i++) { Permission permission = mPermissions.valueAt(i); + if (filterPermissions != null + && !ArrayUtils.contains(filterPermissions, permission.getName())) { + continue; + } if (mAppSupportsRuntimePermissions) { if (permission.isGranted()) { return true; } - } else if (permission.isGranted() && ((permission.getAppOp() - != AppOpsManager.OP_NONE && permission.isAppOpAllowed()) - || permission.getAppOp() == AppOpsManager.OP_NONE)) { + } else if (permission.isGranted() && (permission.getAppOp() == null + || permission.isAppOpAllowed())) { return true; } } @@ -285,13 +313,21 @@ public final class AppPermissionGroup implements Comparable<AppPermissionGroup> } public boolean grantRuntimePermissions(boolean fixedByTheUser) { - final boolean isSharedUser = mPackageInfo.sharedUserId != null; + return grantRuntimePermissions(fixedByTheUser, null); + } + + public boolean grantRuntimePermissions(boolean fixedByTheUser, String[] filterPermissions) { final int uid = mPackageInfo.applicationInfo.uid; // We toggle permissions only to apps that support runtime // permissions, otherwise we toggle the app op corresponding // to the permission if the permission is granted to the app. for (Permission permission : mPermissions.values()) { + if (filterPermissions != null + && !ArrayUtils.contains(filterPermissions, permission.getName())) { + continue; + } + if (mAppSupportsRuntimePermissions) { // Do not touch permissions fixed by the system. if (permission.isSystemFixed()) { @@ -327,43 +363,42 @@ public final class AppPermissionGroup implements Comparable<AppPermissionGroup> } } else { // Legacy apps cannot have a not granted permission but just in case. - // Also if the permissions has no corresponding app op, then it is a - // third-party one and we do not offer toggling of such permissions. - if (!permission.isGranted() || !permission.hasAppOp()) { + if (!permission.isGranted()) { continue; } - if (!permission.isAppOpAllowed()) { - permission.setAppOpAllowed(true); - // It this is a shared user we want to enable the app op for all - // packages in the shared user to match the behavior of this - // shared user having a runtime permission. - if (isSharedUser) { - // Enable the app op. - String[] packageNames = mPackageManager.getPackagesForUid(uid); - for (String packageName : packageNames) { - mAppOps.setUidMode(permission.getAppOp(), uid, - AppOpsManager.MODE_ALLOWED); - } - } else { + int killUid = -1; + int mask = 0; + + // If the permissions has no corresponding app op, then it is a + // third-party one and we do not offer toggling of such permissions. + if (permission.hasAppOp()) { + if (!permission.isAppOpAllowed()) { + permission.setAppOpAllowed(true); // Enable the app op. mAppOps.setUidMode(permission.getAppOp(), uid, AppOpsManager.MODE_ALLOWED); + + // Legacy apps do not know that they have to retry access to a + // resource due to changes in runtime permissions (app ops in this + // case). Therefore, we restart them on app op change, so they + // can pick up the change. + killUid = uid; } // Mark that the permission should not be be granted on upgrade // when the app begins supporting runtime permissions. if (permission.shouldRevokeOnUpgrade()) { permission.setRevokeOnUpgrade(false); - mPackageManager.updatePermissionFlags(permission.getName(), - mPackageInfo.packageName, - PackageManager.FLAG_PERMISSION_REVOKE_ON_UPGRADE, - 0, mUserHandle); + mask |= PackageManager.FLAG_PERMISSION_REVOKE_ON_UPGRADE; } + } + + if (mask != 0) { + mPackageManager.updatePermissionFlags(permission.getName(), + mPackageInfo.packageName, mask, 0, mUserHandle); + } - // Legacy apps do not know that they have to retry access to a - // resource due to changes in runtime permissions (app ops in this - // case). Therefore, we restart them on app op change, so they - // can pick up the change. + if (killUid != -1) { mActivityManager.killUid(uid, KILL_REASON_APP_OP_CHANGE); } } @@ -373,13 +408,21 @@ public final class AppPermissionGroup implements Comparable<AppPermissionGroup> } public boolean revokeRuntimePermissions(boolean fixedByTheUser) { - final boolean isSharedUser = mPackageInfo.sharedUserId != null; + return revokeRuntimePermissions(fixedByTheUser, null); + } + + public boolean revokeRuntimePermissions(boolean fixedByTheUser, String[] filterPermissions) { final int uid = mPackageInfo.applicationInfo.uid; // We toggle permissions only to apps that support runtime // permissions, otherwise we toggle the app op corresponding // to the permission if the permission is granted to the app. for (Permission permission : mPermissions.values()) { + if (filterPermissions != null + && !ArrayUtils.contains(filterPermissions, permission.getName())) { + continue; + } + if (mAppSupportsRuntimePermissions) { // Do not touch permissions fixed by the system. if (permission.isSystemFixed()) { @@ -419,43 +462,43 @@ public final class AppPermissionGroup implements Comparable<AppPermissionGroup> } } else { // Legacy apps cannot have a non-granted permission but just in case. - // Also if the permission has no corresponding app op, then it is a - // third-party one and we do not offer toggling of such permissions. - if (!permission.isGranted() || !permission.hasAppOp()) { + if (!permission.isGranted()) { continue; } - if (permission.isAppOpAllowed()) { - permission.setAppOpAllowed(false); - // It this is a shared user we want to enable the app op for all - // packages the the shared user to match the behavior of this - // shared user having a runtime permission. - if (isSharedUser) { - String[] packageNames = mPackageManager.getPackagesForUid(uid); - for (String packageName : packageNames) { - // Disable the app op. - mAppOps.setUidMode(permission.getAppOp(), uid, - AppOpsManager.MODE_IGNORED); - } - } else { + int mask = 0; + int flags = 0; + int killUid = -1; + + // If the permission has no corresponding app op, then it is a + // third-party one and we do not offer toggling of such permissions. + if (permission.hasAppOp()) { + if (permission.isAppOpAllowed()) { + permission.setAppOpAllowed(false); // Disable the app op. mAppOps.setUidMode(permission.getAppOp(), uid, AppOpsManager.MODE_IGNORED); + + // Disabling an app op may put the app in a situation in which it + // has a handle to state it shouldn't have, so we have to kill the + // app. This matches the revoke runtime permission behavior. + killUid = uid; } // Mark that the permission should not be granted on upgrade // when the app begins supporting runtime permissions. if (!permission.shouldRevokeOnUpgrade()) { permission.setRevokeOnUpgrade(true); - mPackageManager.updatePermissionFlags(permission.getName(), - mPackageInfo.packageName, - PackageManager.FLAG_PERMISSION_REVOKE_ON_UPGRADE, - PackageManager.FLAG_PERMISSION_REVOKE_ON_UPGRADE, - mUserHandle); + mask |= PackageManager.FLAG_PERMISSION_REVOKE_ON_UPGRADE; + flags |= PackageManager.FLAG_PERMISSION_REVOKE_ON_UPGRADE; } + } + + if (mask != 0) { + mPackageManager.updatePermissionFlags(permission.getName(), + mPackageInfo.packageName, mask, flags, mUserHandle); + } - // Disabling an app op may put the app in a situation in which it - // has a handle to state it shouldn't have, so we have to kill the - // app. This matches the revoke runtime permission behavior. + if (killUid != -1) { mActivityManager.killUid(uid, KILL_REASON_APP_OP_CHANGE); } } diff --git a/src/com/android/packageinstaller/permission/model/AppPermissions.java b/src/com/android/packageinstaller/permission/model/AppPermissions.java index a0f23d64..e455ef13 100644 --- a/src/com/android/packageinstaller/permission/model/AppPermissions.java +++ b/src/com/android/packageinstaller/permission/model/AppPermissions.java @@ -19,6 +19,7 @@ package com.android.packageinstaller.permission.model; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.os.Build; import android.text.BidiFormatter; import android.text.TextPaint; import android.text.TextUtils; @@ -31,16 +32,6 @@ import java.util.LinkedHashMap; import java.util.List; public final class AppPermissions { - private static final float MAX_APP_LABEL_LENGTH_PIXELS = 500; - - private static final TextPaint sAppLabelEllipsizePaint = new TextPaint(); - static { - sAppLabelEllipsizePaint.setAntiAlias(true); - // Both text size and width are given in absolute pixels, for consistent truncation - // across devices; this value corresponds to the default 14dip size on an xdhpi device. - sAppLabelEllipsizePaint.setTextSize(42); - } - private final ArrayList<AppPermissionGroup> mGroups = new ArrayList<>(); private final LinkedHashMap<String, AppPermissionGroup> mNameToGroupMap = new LinkedHashMap<>(); @@ -62,7 +53,9 @@ public final class AppPermissions { mContext = context; mPackageInfo = packageInfo; mFilterPermissions = permissions; - mAppLabel = loadEllipsizedAppLabel(context, packageInfo); + mAppLabel = BidiFormatter.getInstance().unicodeWrap( + packageInfo.applicationInfo.loadSafeLabel( + context.getPackageManager()).toString()); mSortGroups = sortGroups; mOnErrorCallback = onErrorCallback; loadPermissionGroups(); @@ -89,6 +82,19 @@ public final class AppPermissions { return mGroups; } + public boolean isReviewRequired() { + if (!Build.PERMISSIONS_REVIEW_REQUIRED) { + return false; + } + final int groupCount = mGroups.size(); + for (int i = 0; i < groupCount; i++) { + AppPermissionGroup group = mGroups.get(i); + if (group.isReviewRequired()) { + return true; + } + } + return false; + } private void loadPackageInfo() { try { @@ -163,16 +169,4 @@ public final class AppPermissions { } return false; } - - private static CharSequence loadEllipsizedAppLabel(Context context, PackageInfo packageInfo) { - String label = packageInfo.applicationInfo.loadLabel( - context.getPackageManager()).toString(); - String ellipsizedLabel = label.replace("\n", " "); - if (!DeviceUtils.isWear(context)) { - // Only ellipsize for non-Wear devices. - ellipsizedLabel = TextUtils.ellipsize(ellipsizedLabel, sAppLabelEllipsizePaint, - MAX_APP_LABEL_LENGTH_PIXELS, TextUtils.TruncateAt.END).toString(); - } - return BidiFormatter.getInstance().unicodeWrap(ellipsizedLabel); - } } diff --git a/src/com/android/packageinstaller/permission/model/Permission.java b/src/com/android/packageinstaller/permission/model/Permission.java index 1be4e75b..f9dc6e8e 100644 --- a/src/com/android/packageinstaller/permission/model/Permission.java +++ b/src/com/android/packageinstaller/permission/model/Permission.java @@ -16,19 +16,18 @@ package com.android.packageinstaller.permission.model; -import android.app.AppOpsManager; import android.content.pm.PackageManager; public final class Permission { private final String mName; - private final int mAppOp; + private final String mAppOp; private boolean mGranted; private boolean mAppOpAllowed; private int mFlags; public Permission(String name, boolean granted, - int appOp, boolean appOpAllowed, int flags) { + String appOp, boolean appOpAllowed, int flags) { mName = name; mGranted = granted; mAppOp = appOp; @@ -40,7 +39,7 @@ public final class Permission { return mName; } - public int getAppOp() { + public String getAppOp() { return mAppOp; } @@ -49,13 +48,21 @@ public final class Permission { } public boolean hasAppOp() { - return mAppOp != AppOpsManager.OP_NONE; + return mAppOp != null; } public boolean isGranted() { return mGranted; } + public boolean isReviewRequired() { + return (mFlags & PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED) != 0; + } + + public void resetReviewRequired() { + mFlags &= ~PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED; + } + public void setGranted(boolean mGranted) { this.mGranted = mGranted; } diff --git a/src/com/android/packageinstaller/permission/model/PermissionApps.java b/src/com/android/packageinstaller/permission/model/PermissionApps.java index e5d96d55..be32f2ac 100644 --- a/src/com/android/packageinstaller/permission/model/PermissionApps.java +++ b/src/com/android/packageinstaller/permission/model/PermissionApps.java @@ -24,6 +24,7 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PermissionInfo; import android.graphics.drawable.Drawable; import android.os.AsyncTask; +import android.os.Process; import android.os.UserHandle; import android.os.UserManager; import android.util.ArrayMap; @@ -35,7 +36,6 @@ import com.android.packageinstaller.R; import com.android.packageinstaller.permission.utils.Utils; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.List; @@ -120,7 +120,7 @@ public class PermissionApps { return count; } - public Collection<PermissionApp> getApps() { + public List<PermissionApp> getApps() { return mPermApps; } @@ -149,9 +149,10 @@ public class PermissionApps { ArrayList<PermissionApp> permApps = new ArrayList<>(); - for (UserHandle user : UserManager.get(mContext).getUserProfiles()) { + UserManager userManager = mContext.getSystemService(UserManager.class); + for (UserHandle user : userManager.getUserProfiles()) { List<PackageInfo> apps = mCache != null ? mCache.getPackages(user.getIdentifier()) - : mPm.getInstalledPackages(PackageManager.GET_PERMISSIONS, + : mPm.getInstalledPackagesAsUser(PackageManager.GET_PERMISSIONS, user.getIdentifier()); final int N = apps.size(); @@ -182,17 +183,31 @@ public class PermissionApps { || (requestedPermissionInfo.flags & PermissionInfo.FLAG_INSTALLED) == 0 || (requestedPermissionInfo.flags - & PermissionInfo.FLAG_HIDDEN) != 0) { + & PermissionInfo.FLAG_REMOVED) != 0) { continue; } AppPermissionGroup group = AppPermissionGroup.create(mContext, app, groupInfo, groupPermInfos, user); + if (group == null) { + continue; + } + String label = mSkipUi ? app.packageName : app.applicationInfo.loadLabel(mPm).toString(); - PermissionApp permApp = new PermissionApp(app.packageName, - group, label, getBadgedIcon(app.applicationInfo), + + Drawable icon = null; + if (!mSkipUi) { + UserHandle userHandle = new UserHandle( + UserHandle.getUserId(group.getApp().applicationInfo.uid)); + + icon = mPm.getUserBadgedIcon( + mPm.loadUnbadgedItemIcon(app.applicationInfo, app.applicationInfo), + userHandle); + } + + PermissionApp permApp = new PermissionApp(app.packageName, group, label, icon, app.applicationInfo); permApps.add(permApp); @@ -246,15 +261,6 @@ public class PermissionApps { return null; } - private Drawable getBadgedIcon(ApplicationInfo appInfo) { - if (mSkipUi) { - return null; - } - Drawable unbadged = appInfo.loadUnbadgedIcon(mPm); - return mPm.getUserBadgedIcon(unbadged, - new UserHandle(UserHandle.getUserId(appInfo.uid))); - } - private void loadGroupInfo() { PackageItemInfo info; try { @@ -317,6 +323,10 @@ public class PermissionApps { return mAppPermissionGroup.areRuntimePermissionsGranted(); } + public boolean isReviewRequired() { + return mAppPermissionGroup.isReviewRequired(); + } + public void grantRuntimePermissions() { mAppPermissionGroup.grantRuntimePermissions(false); } @@ -341,8 +351,8 @@ public class PermissionApps { return mAppPermissionGroup.hasRuntimePermission(); } - public boolean hasAppOpPermissions() { - return mAppPermissionGroup.hasAppOpPermission(); + public int getUserId() { + return mAppPermissionGroup.getUserId(); } public String getPackageName() { @@ -401,7 +411,7 @@ public class PermissionApps { public synchronized List<PackageInfo> getPackages(int userId) { List<PackageInfo> ret = mPackageInfoCache.get(userId); if (ret == null) { - ret = mPm.getInstalledPackages(PackageManager.GET_PERMISSIONS, userId); + ret = mPm.getInstalledPackagesAsUser(PackageManager.GET_PERMISSIONS, userId); mPackageInfoCache.put(userId, ret); } return ret; diff --git a/src/com/android/packageinstaller/permission/model/PermissionGroups.java b/src/com/android/packageinstaller/permission/model/PermissionGroups.java index c496e898..8ca69f24 100644 --- a/src/com/android/packageinstaller/permission/model/PermissionGroups.java +++ b/src/com/android/packageinstaller/permission/model/PermissionGroups.java @@ -131,7 +131,7 @@ public final class PermissionGroups implements LoaderCallbacks<List<PermissionGr seenPermissions.add(groupPermission.name); if (groupPermission.protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && (groupPermission.flags & PermissionInfo.FLAG_INSTALLED) != 0 - && (groupPermission.flags & PermissionInfo.FLAG_HIDDEN) == 0) { + && (groupPermission.flags & PermissionInfo.FLAG_REMOVED) == 0) { hasRuntimePermissions = true; } } diff --git a/src/com/android/packageinstaller/permission/model/PermissionStatusReceiver.java b/src/com/android/packageinstaller/permission/model/PermissionStatusReceiver.java index 810ae8ec..a87976a6 100644 --- a/src/com/android/packageinstaller/permission/model/PermissionStatusReceiver.java +++ b/src/com/android/packageinstaller/permission/model/PermissionStatusReceiver.java @@ -33,12 +33,110 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +/** + * This class handles backwards compatibility for M. Don't remove + * until we decide to drop M support altogether. + */ public class PermissionStatusReceiver extends BroadcastReceiver { + + /** + * Broadcast action that requests current permission granted information. It will respond + * to the request by sending a broadcast with action defined by + * {@link #EXTRA_GET_PERMISSIONS_RESPONSE_INTENT}. The response will contain + * {@link #EXTRA_GET_PERMISSIONS_COUNT_RESULT}, as well as + * {@link #EXTRA_GET_PERMISSIONS_GROUP_LIST_RESULT}, with contents described below or + * a null upon failure. + * + * <p>If {@link Intent#EXTRA_PACKAGE_NAME} is included then the number of permissions granted, the + * number of permissions requested and the number of granted additional permissions + * by that package will be calculated and included as the first + * and second elements respectively of an int[] in the response as + * {@link #EXTRA_GET_PERMISSIONS_COUNT_RESULT}. The response will also deliver the list + * of localized permission group names that are granted in + * {@link #EXTRA_GET_PERMISSIONS_GROUP_LIST_RESULT}. + * + * <p>If {@link #EXTRA_PACKAGE_NAME} is not included then the number of apps granted any runtime + * permissions and the total number of apps requesting runtime permissions will be the first + * and second elements respectively of an int[] in the response as + * {@link #EXTRA_GET_PERMISSIONS_COUNT_RESULT}. + * + * @hide + */ + public static final String ACTION_GET_PERMISSIONS_COUNT + = "android.intent.action.GET_PERMISSIONS_COUNT"; + + /** + * Broadcast action that requests list of all apps that have runtime permissions. It will + * respond to the request by sending a broadcast with action defined by + * {@link #EXTRA_GET_PERMISSIONS_PACKAGES_RESPONSE_INTENT}. The response will contain + * {@link #EXTRA_GET_PERMISSIONS_APP_LIST_RESULT}, as well as + * {@link #EXTRA_GET_PERMISSIONS_APP_LABEL_LIST_RESULT}, with contents described below or + * a null upon failure. + * + * <p>{@link #EXTRA_GET_PERMISSIONS_APP_LIST_RESULT} will contain a list of package names of + * apps that have runtime permissions. {@link #EXTRA_GET_PERMISSIONS_APP_LABEL_LIST_RESULT} + * will contain the list of app labels corresponding ot the apps in the first list. + * + * @hide + */ + public static final String ACTION_GET_PERMISSIONS_PACKAGES + = "android.intent.action.GET_PERMISSIONS_PACKAGES"; + + /** + * Extra included in response to {@link #ACTION_GET_PERMISSIONS_COUNT}. + * @hide + */ + public static final String EXTRA_GET_PERMISSIONS_COUNT_RESULT + = "android.intent.extra.GET_PERMISSIONS_COUNT_RESULT"; + + /** + * List of CharSequence of localized permission group labels. + * @hide + */ + public static final String EXTRA_GET_PERMISSIONS_GROUP_LIST_RESULT + = "android.intent.extra.GET_PERMISSIONS_GROUP_LIST_RESULT"; + + /** + * String list of apps that have one or more runtime permissions. + * @hide + */ + public static final String EXTRA_GET_PERMISSIONS_APP_LIST_RESULT + = "android.intent.extra.GET_PERMISSIONS_APP_LIST_RESULT"; + + /** + * String list of app labels for apps that have one or more runtime permissions. + * @hide + */ + public static final String EXTRA_GET_PERMISSIONS_APP_LABEL_LIST_RESULT + = "android.intent.extra.GET_PERMISSIONS_APP_LABEL_LIST_RESULT"; + + /** + * Boolean list describing if the app is a system app for apps that have one or more runtime + * permissions. + * @hide + */ + public static final String EXTRA_GET_PERMISSIONS_IS_SYSTEM_APP_LIST_RESULT + = "android.intent.extra.GET_PERMISSIONS_IS_SYSTEM_APP_LIST_RESULT"; + + /** + * Required extra to be sent with {@link #ACTION_GET_PERMISSIONS_COUNT} broadcasts. + * @hide + */ + public static final String EXTRA_GET_PERMISSIONS_RESPONSE_INTENT + = "android.intent.extra.GET_PERMISSIONS_RESONSE_INTENT"; + + /** + * Required extra to be sent with {@link #ACTION_GET_PERMISSIONS_PACKAGES} broadcasts. + * @hide + */ + public static final String EXTRA_GET_PERMISSIONS_PACKAGES_RESPONSE_INTENT + = "android.intent.extra.GET_PERMISSIONS_PACKAGES_RESONSE_INTENT"; + @Override public void onReceive(Context context, Intent intent) { - if (Intent.ACTION_GET_PERMISSIONS_COUNT.equals(intent.getAction())) { + if (ACTION_GET_PERMISSIONS_COUNT.equals(intent.getAction())) { Intent responseIntent = new Intent(intent.getStringExtra( - Intent.EXTRA_GET_PERMISSIONS_RESPONSE_INTENT)); + EXTRA_GET_PERMISSIONS_RESPONSE_INTENT)); responseIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); int[] counts = new int[3]; @@ -54,28 +152,28 @@ public class PermissionStatusReceiver extends BroadcastReceiver { succeeded = getAppsWithPermissionsCount(context, counts); } if (succeeded) { - responseIntent.putExtra(Intent.EXTRA_GET_PERMISSIONS_COUNT_RESULT, counts); + responseIntent.putExtra(EXTRA_GET_PERMISSIONS_COUNT_RESULT, counts); if (isForPackage) { - responseIntent.putExtra(Intent.EXTRA_GET_PERMISSIONS_GROUP_LIST_RESULT, + responseIntent.putExtra(EXTRA_GET_PERMISSIONS_GROUP_LIST_RESULT, grantedGroups.toArray(new CharSequence[grantedGroups.size()])); } } context.sendBroadcast(responseIntent); - } else if (Intent.ACTION_GET_PERMISSIONS_PACKAGES.equals(intent.getAction())) { + } else if (ACTION_GET_PERMISSIONS_PACKAGES.equals(intent.getAction())) { Intent responseIntent = new Intent(intent.getStringExtra( - Intent.EXTRA_GET_PERMISSIONS_PACKAGES_RESPONSE_INTENT)); + EXTRA_GET_PERMISSIONS_PACKAGES_RESPONSE_INTENT)); responseIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); List<String> appsList = new ArrayList<>(); List<CharSequence> appLabelsList = new ArrayList<>(); List<Boolean> isSystemAppList = new ArrayList<>(); if (getAppsWithRuntimePermissions(context, appsList, appLabelsList, isSystemAppList)) { - responseIntent.putExtra(Intent.EXTRA_GET_PERMISSIONS_APP_LIST_RESULT, + responseIntent.putExtra(EXTRA_GET_PERMISSIONS_APP_LIST_RESULT, appsList.toArray(new String[appsList.size()])); - responseIntent.putExtra(Intent.EXTRA_GET_PERMISSIONS_APP_LABEL_LIST_RESULT, + responseIntent.putExtra(EXTRA_GET_PERMISSIONS_APP_LABEL_LIST_RESULT, appLabelsList.toArray(new String[appLabelsList.size()])); - responseIntent.putExtra(Intent.EXTRA_GET_PERMISSIONS_IS_SYSTEM_APP_LIST_RESULT, + responseIntent.putExtra(EXTRA_GET_PERMISSIONS_IS_SYSTEM_APP_LIST_RESULT, toPrimitiveBoolArray(isSystemAppList)); } context.sendBroadcast(responseIntent); diff --git a/src/com/android/packageinstaller/permission/service/RuntimePermissionPresenterServiceImpl.java b/src/com/android/packageinstaller/permission/service/RuntimePermissionPresenterServiceImpl.java new file mode 100644 index 00000000..fbd4f10c --- /dev/null +++ b/src/com/android/packageinstaller/permission/service/RuntimePermissionPresenterServiceImpl.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.packageinstaller.permission.service; + +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.permission.RuntimePermissionPresentationInfo; +import android.permissionpresenterservice.RuntimePermissionPresenterService; +import android.util.ArraySet; +import android.util.Log; +import com.android.packageinstaller.permission.model.AppPermissionGroup; +import com.android.packageinstaller.permission.model.AppPermissions; +import com.android.packageinstaller.permission.utils.Utils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Service that provides presentation information for runtime permissions. + */ +public final class RuntimePermissionPresenterServiceImpl extends RuntimePermissionPresenterService { + private static final String LOG_TAG = "PermissionPresenter"; + + @Override + public List<RuntimePermissionPresentationInfo> onGetAppPermissions(String packageName) { + final PackageInfo packageInfo; + try { + packageInfo = getPackageManager().getPackageInfo(packageName, + PackageManager.GET_PERMISSIONS); + } catch (PackageManager.NameNotFoundException e) { + Log.e(LOG_TAG, "Error getting package:" + packageName, e); + return null; + } + + List<RuntimePermissionPresentationInfo> permissions = new ArrayList<>(); + + AppPermissions appPermissions = new AppPermissions(this, packageInfo, null, false, null); + for (AppPermissionGroup group : appPermissions.getPermissionGroups()) { + if (Utils.shouldShowPermission(group, packageName)) { + final boolean granted = group.areRuntimePermissionsGranted(); + final boolean standard = Utils.OS_PKG.equals(group.getDeclaringPackage()); + RuntimePermissionPresentationInfo permission = + new RuntimePermissionPresentationInfo(group.getLabel(), + granted, standard); + permissions.add(permission); + } + } + + return permissions; + } + + @Override + public List<ApplicationInfo> onGetAppsUsingPermissions(boolean system) { + final List<ApplicationInfo> appInfos = Utils.getAllInstalledApplications(this); + if (appInfos == null || appInfos.isEmpty()) { + return null; + } + List<ApplicationInfo> appsResult = new ArrayList<>(); + ArraySet<String> launcherPackages = Utils.getLauncherPackages(this); + final int appInfosSize = appInfos.size(); + for (int i = 0; i < appInfosSize; i++) { + ApplicationInfo appInfo = appInfos.get(i); + final String packageName = appInfo.packageName; + final PackageInfo packageInfo; + try { + packageInfo = getPackageManager().getPackageInfo( + packageName, PackageManager.GET_PERMISSIONS); + } catch (PackageManager.NameNotFoundException e) { + Log.e(LOG_TAG, "Error getting package info for:" + packageName, e); + continue; + + } + AppPermissions appPermissions = new AppPermissions(this, + packageInfo, null, false, null); + boolean shouldShow = false; + + + for (AppPermissionGroup group : appPermissions.getPermissionGroups()) { + if (Utils.shouldShowPermission(group, packageName)) { + shouldShow = true; + break; + } + } + if (shouldShow) { + if (Utils.isSystem(appPermissions, launcherPackages) == system) { + appsResult.add(appInfo); + } + } + } + return appsResult; + } +} diff --git a/src/com/android/packageinstaller/permission/ui/ConfirmActionDialogFragment.java b/src/com/android/packageinstaller/permission/ui/ConfirmActionDialogFragment.java new file mode 100644 index 00000000..d6d79001 --- /dev/null +++ b/src/com/android/packageinstaller/permission/ui/ConfirmActionDialogFragment.java @@ -0,0 +1,63 @@ +/* + * 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.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.DialogInterface; +import android.os.Bundle; +import com.android.packageinstaller.R; + +public final class ConfirmActionDialogFragment extends DialogFragment { + public static final String ARG_MESSAGE = "MESSAGE"; + public static final String ARG_ACTION = "ACTION"; + + public static interface OnActionConfirmedListener { + public void onActionConfirmed(String action); + } + + public static ConfirmActionDialogFragment newInstance(CharSequence message, String action) { + Bundle arguments = new Bundle(); + arguments.putCharSequence(ARG_MESSAGE, message); + arguments.putString(ARG_ACTION, action); + ConfirmActionDialogFragment fragment = new ConfirmActionDialogFragment(); + fragment.setArguments(arguments); + return fragment; + } + + @Override + public Dialog onCreateDialog(Bundle bundle) { + return new AlertDialog.Builder(getContext()) + .setMessage(getArguments().getString(ARG_MESSAGE)) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.grant_dialog_button_deny_anyway, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Activity activity = getActivity(); + if (activity instanceof OnActionConfirmedListener) { + String groupName = getArguments().getString(ARG_ACTION); + ((OnActionConfirmedListener) activity) + .onActionConfirmed(groupName); + } + } + }) + .create(); + } +} diff --git a/src/com/android/packageinstaller/permission/ui/GrantPermissionsActivity.java b/src/com/android/packageinstaller/permission/ui/GrantPermissionsActivity.java index 102fd6ef..4ee76a18 100644 --- a/src/com/android/packageinstaller/permission/ui/GrantPermissionsActivity.java +++ b/src/com/android/packageinstaller/permission/ui/GrantPermissionsActivity.java @@ -25,13 +25,14 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PermissionInfo; +import android.content.res.Configuration; import android.content.res.Resources; -import android.graphics.Typeface; import android.graphics.drawable.Icon; import android.hardware.camera2.utils.ArrayUtils; +import android.os.Build; import android.os.Bundle; -import android.text.SpannableString; -import android.text.style.StyleSpan; +import android.text.Html; +import android.text.Spanned; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; @@ -44,8 +45,8 @@ import com.android.packageinstaller.R; import com.android.packageinstaller.permission.model.AppPermissionGroup; import com.android.packageinstaller.permission.model.AppPermissions; import com.android.packageinstaller.permission.model.Permission; +import com.android.packageinstaller.permission.ui.handheld.GrantPermissionsViewHandlerImpl; import com.android.packageinstaller.permission.utils.SafetyNetLogger; -import com.android.packageinstaller.permission.utils.Utils; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -99,17 +100,27 @@ public class GrantPermissionsActivity extends OverlayTouchActivity PackageInfo callingPackageInfo = getCallingPackageInfo(); + if (callingPackageInfo == null || callingPackageInfo.requestedPermissions == null + || callingPackageInfo.requestedPermissions.length <= 0) { + setResultAndFinish(); + return; + } + + // Don't allow legacy apps to request runtime permissions. + if (callingPackageInfo.applicationInfo.targetSdkVersion < Build.VERSION_CODES.M) { + // Returning empty arrays means a cancellation. + mRequestedPermissions = new String[0]; + mGrantResults = new int[0]; + setResultAndFinish(); + return; + } + DevicePolicyManager devicePolicyManager = getSystemService(DevicePolicyManager.class); final int permissionPolicy = devicePolicyManager.getPermissionPolicy(null); // If calling package is null we default to deny all. updateDefaultResults(callingPackageInfo, permissionPolicy); - if (callingPackageInfo == null) { - setResultAndFinish(); - return; - } - mAppPermissions = new AppPermissions(this, callingPackageInfo, null, false, new Runnable() { @Override @@ -118,15 +129,15 @@ public class GrantPermissionsActivity extends OverlayTouchActivity } }); - for (AppPermissionGroup group : mAppPermissions.getPermissionGroups()) { - boolean groupHasRequestedPermission = false; - for (String requestedPermission : mRequestedPermissions) { - if (group.hasPermission(requestedPermission)) { - groupHasRequestedPermission = true; + for (String requestedPermission : mRequestedPermissions) { + AppPermissionGroup group = null; + for (AppPermissionGroup nextGroup : mAppPermissions.getPermissionGroups()) { + if (nextGroup.hasPermission(requestedPermission)) { + group = nextGroup; break; } } - if (!groupHasRequestedPermission) { + if (group == null) { continue; } // We allow the user to choose only non-fixed permissions. A permission @@ -176,6 +187,21 @@ public class GrantPermissionsActivity extends OverlayTouchActivity } @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + // We need to relayout the window as dialog width may be + // different in landscape vs portrait which affect the min + // window height needed to show all content. We have to + // re-add the window to force it to be resized if needed. + View decor = getWindow().getDecorView(); + getWindowManager().removeViewImmediate(decor); + getWindowManager().addView(decor, decor.getLayoutParams()); + if (mViewHandler instanceof GrantPermissionsViewHandlerImpl) { + ((GrantPermissionsViewHandlerImpl) mViewHandler).onConfigurationChanged(); + } + } + + @Override public boolean dispatchTouchEvent(MotionEvent ev) { View rootView = getWindow().getDecorView(); if (rootView.getTop() != 0) { @@ -204,16 +230,10 @@ public class GrantPermissionsActivity extends OverlayTouchActivity for (GroupState groupState : mRequestGrantPermissionGroups.values()) { if (groupState.mState == GroupState.STATE_UNKNOWN) { CharSequence appLabel = mAppPermissions.getAppLabel(); - SpannableString message = new SpannableString(getString( - R.string.permission_warning_template, appLabel, - groupState.mGroup.getDescription())); + Spanned message = Html.fromHtml(getString(R.string.permission_warning_template, + appLabel, groupState.mGroup.getDescription()), 0); // Set the permission message as the title so it can be announced. setTitle(message); - // Color the app name. - int appLabelStart = message.toString().indexOf(appLabel.toString(), 0); - int appLabelLength = appLabel.length(); - message.setSpan(new StyleSpan(Typeface.BOLD), appLabelStart, - appLabelStart + appLabelLength, 0); // Set the new grant view // TODO: Use a real message for the action. We need group action APIs @@ -241,11 +261,6 @@ public class GrantPermissionsActivity extends OverlayTouchActivity @Override public void onPermissionGrantResult(String name, boolean granted, boolean doNotAskAgain) { - if (isObscuredTouch()) { - showOverlayDialog(); - finish(); - return; - } GroupState groupState = mRequestGrantPermissionGroups.get(name); if (groupState.mGroup != null) { if (granted) { diff --git a/src/com/android/packageinstaller/permission/ui/ManualLayoutFrame.java b/src/com/android/packageinstaller/permission/ui/ManualLayoutFrame.java index c9ccf9c1..a20c9523 100644 --- a/src/com/android/packageinstaller/permission/ui/ManualLayoutFrame.java +++ b/src/com/android/packageinstaller/permission/ui/ManualLayoutFrame.java @@ -18,58 +18,56 @@ package com.android.packageinstaller.permission.ui; import android.content.Context; import android.util.AttributeSet; import android.view.View; -import android.widget.FrameLayout; +import android.view.ViewGroup; -/** - * Allows one standard layout pass, but afterwards holds getMeasuredHeight constant, - * however still allows drawing larger at the size needed by its children. This allows - * a dialog to tell the window the height is constant (with keeps its position constant) - * but allows the view to grow downwards for animation. - */ -public class ManualLayoutFrame extends FrameLayout { - - private int mDesiredHeight; - private int mHeight; +public class ManualLayoutFrame extends ViewGroup { + private int mContentBottom; private int mWidth; - private View mOffsetView; - public ManualLayoutFrame(Context context, AttributeSet attrs) { super(context, attrs); - setClipChildren(false); - setClipToPadding(false); } - public int getLayoutHeight() { - return mDesiredHeight; + public void onConfigurationChanged() { + mContentBottom = 0; + mWidth = 0; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mWidth != 0) { - // Keep the width constant to avoid weirdness. + int newWidth = mWidth; + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + switch (widthMode) { + case MeasureSpec.AT_MOST: { + newWidth = Math.min(mWidth, MeasureSpec.getSize(widthMeasureSpec)); + } break; + case MeasureSpec.EXACTLY: { + newWidth = MeasureSpec.getSize(widthMeasureSpec); + } break; + } + if (newWidth != mWidth) { + mWidth = newWidth; + } widthMeasureSpec = MeasureSpec.makeMeasureSpec(mWidth, MeasureSpec.EXACTLY); } super.onMeasure(widthMeasureSpec, heightMeasureSpec); - mDesiredHeight = getMeasuredHeight(); - if (mHeight == 0 && mDesiredHeight != 0) { - // Record the first non-zero width and height, this will be the height henceforth. - mHeight = mDesiredHeight; + if (mWidth == 0) { mWidth = getMeasuredWidth(); } - if (mHeight != 0) { - // Always report the same height - setMeasuredDimension(getMeasuredWidth(), mHeight); - } + + measureChild(getChildAt(0), widthMeasureSpec, heightMeasureSpec); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - if (mDesiredHeight != 0) { - // Draw at height we expect to be. - setBottom(getTop() + mDesiredHeight); - bottom = top + mDesiredHeight; + View content = getChildAt(0); + if (mContentBottom == 0) { + mContentBottom = (getMeasuredHeight() + content.getMeasuredHeight()) / 2; } - super.onLayout(changed, left, top, right, bottom); + final int contentLeft = (getMeasuredWidth() - content.getMeasuredWidth()) / 2; + final int contentTop = mContentBottom - content.getMeasuredHeight(); + final int contentRight = contentLeft + content.getMeasuredWidth(); + content.layout(contentLeft, contentTop, contentRight, mContentBottom); } } diff --git a/src/com/android/packageinstaller/permission/ui/OverlayTouchActivity.java b/src/com/android/packageinstaller/permission/ui/OverlayTouchActivity.java index b3938b1e..a7800ca5 100644 --- a/src/com/android/packageinstaller/permission/ui/OverlayTouchActivity.java +++ b/src/com/android/packageinstaller/permission/ui/OverlayTouchActivity.java @@ -15,35 +15,33 @@ */ package com.android.packageinstaller.permission.ui; -import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; - import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.view.MotionEvent; +import android.app.AppOpsManager; +import android.os.Binder; +import android.os.IBinder; public class OverlayTouchActivity extends Activity { - - private boolean mObscuredTouch; - - public boolean isObscuredTouch() { - return mObscuredTouch; - } + private final IBinder mToken = new Binder(); @Override - protected void onCreate(Bundle savedInstanceState) { - getWindow().addPrivateFlags(PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); - super.onCreate(savedInstanceState); + protected void onResume() { + super.onResume(); + setOverlayAllowed(false); } @Override - public boolean dispatchTouchEvent(MotionEvent event) { - mObscuredTouch = (event.getFlags() & (MotionEvent.FLAG_WINDOW_IS_OBSCURED - | MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED)) != 0; - return super.dispatchTouchEvent(event); + protected void onPause() { + super.onPause(); + setOverlayAllowed(true); } - public void showOverlayDialog() { - startActivity(new Intent(this, OverlayWarningDialog.class)); + private void setOverlayAllowed(boolean allowed) { + AppOpsManager appOpsManager = getSystemService(AppOpsManager.class); + if (appOpsManager != null) { + appOpsManager.setUserRestriction(AppOpsManager.OP_SYSTEM_ALERT_WINDOW, + !allowed, mToken); + appOpsManager.setUserRestriction(AppOpsManager.OP_TOAST_WINDOW, + !allowed, mToken); + } } } diff --git a/src/com/android/packageinstaller/permission/ui/ReviewPermissionsActivity.java b/src/com/android/packageinstaller/permission/ui/ReviewPermissionsActivity.java new file mode 100644 index 00000000..6bc251ae --- /dev/null +++ b/src/com/android/packageinstaller/permission/ui/ReviewPermissionsActivity.java @@ -0,0 +1,412 @@ +/* + * 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.app.Activity; + +import android.app.DialogFragment; +import android.app.Fragment; +import android.content.Intent; +import android.content.IntentSender; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.RemoteCallback; +import android.preference.Preference; +import android.preference.PreferenceCategory; +import android.preference.PreferenceFragment; +import android.preference.PreferenceGroup; +import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.preference.TwoStatePreference; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.ForegroundColorSpan; +import android.util.TypedValue; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; +import com.android.packageinstaller.R; +import com.android.packageinstaller.permission.model.AppPermissionGroup; +import com.android.packageinstaller.permission.model.AppPermissions; +import com.android.packageinstaller.permission.utils.Utils; +import com.android.packageinstaller.permission.ui.ConfirmActionDialogFragment.OnActionConfirmedListener; + +import java.util.List; + +public final class ReviewPermissionsActivity extends Activity + implements OnActionConfirmedListener { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + PackageInfo packageInfo = getTargetPackageInfo(); + if (packageInfo == null) { + finish(); + return; + } + + setContentView(R.layout.review_permissions); + if (getFragmentManager().findFragmentById(R.id.preferences_frame) == null) { + getFragmentManager().beginTransaction().add(R.id.preferences_frame, + ReviewPermissionsFragment.newInstance(packageInfo)).commit(); + } + } + + @Override + public void onActionConfirmed(String action) { + Fragment fragment = getFragmentManager().findFragmentById(R.id.preferences_frame); + if (fragment instanceof OnActionConfirmedListener) { + ((OnActionConfirmedListener) fragment).onActionConfirmed(action); + } + } + + private PackageInfo getTargetPackageInfo() { + String packageName = getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME); + if (TextUtils.isEmpty(packageName)) { + return null; + } + try { + return getPackageManager().getPackageInfo(packageName, + PackageManager.GET_PERMISSIONS); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + + public static final class ReviewPermissionsFragment extends PreferenceFragment + implements View.OnClickListener, Preference.OnPreferenceChangeListener, + ConfirmActionDialogFragment.OnActionConfirmedListener { + public static final String EXTRA_PACKAGE_INFO = + "com.android.packageinstaller.permission.ui.extra.PACKAGE_INFO"; + + private AppPermissions mAppPermissions; + + private Button mContinueButton; + private Button mCancelButton; + + private PreferenceCategory mNewPermissionsCategory; + + private boolean mHasConfirmedRevoke; + + public static ReviewPermissionsFragment newInstance(PackageInfo packageInfo) { + Bundle arguments = new Bundle(); + arguments.putParcelable(ReviewPermissionsFragment.EXTRA_PACKAGE_INFO, packageInfo); + ReviewPermissionsFragment instance = new ReviewPermissionsFragment(); + instance.setArguments(arguments); + instance.setRetainInstance(true); + return instance; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Activity activity = getActivity(); + if (activity == null) { + return; + } + + PackageInfo packageInfo = getArguments().getParcelable(EXTRA_PACKAGE_INFO); + if (packageInfo == null) { + activity.finish(); + return; + } + + mAppPermissions = new AppPermissions(activity, packageInfo, null, false, + new Runnable() { + @Override + public void run() { + getActivity().finish(); + } + }); + + if (mAppPermissions.getPermissionGroups().isEmpty()) { + activity.finish(); + return; + } + + boolean reviewRequired = false; + for (AppPermissionGroup group : mAppPermissions.getPermissionGroups()) { + if (group.isReviewRequired()) { + reviewRequired = true; + break; + } + } + + if (!reviewRequired) { + activity.finish(); + } + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + bindUi(); + } + + @Override + public void onResume() { + super.onResume(); + mAppPermissions.refresh(); + loadPreferences(); + } + + @Override + public void onClick(View view) { + Activity activity = getActivity(); + if (activity == null) { + return; + } + if (view == mContinueButton) { + confirmPermissionsReview(); + executeCallback(true); + } else if (view == mCancelButton) { + executeCallback(false); + activity.setResult(Activity.RESULT_CANCELED); + } + activity.finish(); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (mHasConfirmedRevoke) { + return true; + } + if (preference instanceof SwitchPreference) { + SwitchPreference switchPreference = (SwitchPreference) preference; + if (switchPreference.isChecked()) { + showWarnRevokeDialog(switchPreference.getKey()); + } else { + return true; + } + } + return false; + } + + @Override + public void onActionConfirmed(String action) { + Preference preference = getPreferenceManager().findPreference(action); + if (preference instanceof SwitchPreference) { + SwitchPreference switchPreference = (SwitchPreference) preference; + switchPreference.setChecked(false); + mHasConfirmedRevoke = true; + } + } + + private void showWarnRevokeDialog(final String groupName) { + DialogFragment fragment = ConfirmActionDialogFragment.newInstance( + getString(R.string.old_sdk_deny_warning), groupName); + fragment.show(getFragmentManager(), fragment.getClass().getName()); + } + + private void confirmPermissionsReview() { + PreferenceGroup preferenceGroup = mNewPermissionsCategory != null + ? mNewPermissionsCategory : getPreferenceScreen(); + + final int preferenceCount = preferenceGroup.getPreferenceCount(); + for (int i = 0; i < preferenceCount; i++) { + Preference preference = preferenceGroup.getPreference(i); + if (preference instanceof TwoStatePreference) { + TwoStatePreference twoStatePreference = (TwoStatePreference) preference; + String groupName = preference.getKey(); + AppPermissionGroup group = mAppPermissions.getPermissionGroup(groupName); + if (twoStatePreference.isChecked()) { + group.grantRuntimePermissions(false); + } else { + group.revokeRuntimePermissions(false); + } + group.resetReviewRequired(); + } + } + } + + private void bindUi() { + Activity activity = getActivity(); + if (activity == null) { + return; + } + + // Set icon + Drawable icon = mAppPermissions.getPackageInfo().applicationInfo.loadIcon( + activity.getPackageManager()); + ImageView iconView = (ImageView) activity.findViewById(R.id.app_icon); + iconView.setImageDrawable(icon); + + // Set message + String appLabel = mAppPermissions.getAppLabel().toString(); + final int labelTemplateResId = isPackageUpdated() + ? R.string.permission_review_title_template_update + : R.string.permission_review_title_template_install; + SpannableString message = new SpannableString(getString(labelTemplateResId, appLabel)); + // Set the permission message as the title so it can be announced. + activity.setTitle(message); + + // Color the app name. + final int appLabelStart = message.toString().indexOf(appLabel, 0); + final int appLabelLength = appLabel.length(); + + TypedValue typedValue = new TypedValue(); + activity.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true); + final int color = activity.getColor(typedValue.resourceId); + + message.setSpan(new ForegroundColorSpan(color), appLabelStart, + appLabelStart + appLabelLength, 0); + TextView permissionsMessageView = (TextView) activity.findViewById( + R.id.permissions_message); + permissionsMessageView.setText(message); + + + mContinueButton = (Button) getActivity().findViewById(R.id.continue_button); + mContinueButton.setOnClickListener(this); + + mCancelButton = (Button) getActivity().findViewById(R.id.cancel_button); + mCancelButton.setOnClickListener(this); + } + + private void loadPreferences() { + Activity activity = getActivity(); + if (activity == null) { + return; + } + + PreferenceScreen screen = getPreferenceScreen(); + if (screen == null) { + screen = getPreferenceManager().createPreferenceScreen(getActivity()); + setPreferenceScreen(screen); + } else { + screen.removeAll(); + } + + PreferenceGroup currentPermissionsCategory = null; + PreferenceGroup oldNewPermissionsCategory = mNewPermissionsCategory; + mNewPermissionsCategory = null; + + final boolean isPackageUpdated = isPackageUpdated(); + + for (AppPermissionGroup group : mAppPermissions.getPermissionGroups()) { + if (!Utils.shouldShowPermission(group, mAppPermissions.getPackageInfo().packageName) + || !Utils.OS_PKG.equals(group.getDeclaringPackage())) { + continue; + } + + // TODO: Sort permissions - platform first then third-party ones + + final SwitchPreference preference; + Preference cachedPreference = oldNewPermissionsCategory != null + ? oldNewPermissionsCategory.findPreference(group.getName()) : null; + if (cachedPreference instanceof SwitchPreference) { + preference = (SwitchPreference) cachedPreference; + } else { + preference = new SwitchPreference(getActivity()); + + preference.setKey(group.getName()); + Drawable icon = Utils.loadDrawable(activity.getPackageManager(), + group.getIconPkg(), group.getIconResId()); + preference.setIcon(Utils.applyTint(getContext(), icon, + android.R.attr.colorControlNormal)); + preference.setTitle(group.getLabel()); + preference.setSummary(group.getDescription()); + preference.setPersistent(false); + + preference.setOnPreferenceChangeListener(this); + } + + preference.setChecked(group.areRuntimePermissionsGranted()); + + // Mutable state + if (group.isPolicyFixed()) { + preference.setEnabled(false); + preference.setSummary(getString( + R.string.permission_summary_enforced_by_policy)); + } else { + preference.setEnabled(true); + } + + if (group.isReviewRequired()) { + if (!isPackageUpdated) { + screen.addPreference(preference); + } else { + if (mNewPermissionsCategory == null) { + mNewPermissionsCategory = new PreferenceCategory(activity); + mNewPermissionsCategory.setTitle(R.string.new_permissions_category); + mNewPermissionsCategory.setOrder(1); + screen.addPreference(mNewPermissionsCategory); + } + mNewPermissionsCategory.addPreference(preference); + } + } else { + if (currentPermissionsCategory == null) { + currentPermissionsCategory = new PreferenceCategory(activity); + currentPermissionsCategory.setTitle(R.string.current_permissions_category); + currentPermissionsCategory.setOrder(2); + screen.addPreference(currentPermissionsCategory); + } + currentPermissionsCategory.addPreference(preference); + } + } + } + + private boolean isPackageUpdated() { + List<AppPermissionGroup> groups = mAppPermissions.getPermissionGroups(); + final int groupCount = groups.size(); + for (int i = 0; i < groupCount; i++) { + AppPermissionGroup group = groups.get(i); + if (!group.isReviewRequired()) { + return true; + } + } + return false; + } + + private void executeCallback(boolean success) { + Activity activity = getActivity(); + if (activity == null) { + return; + } + if (success) { + IntentSender intent = activity.getIntent().getParcelableExtra(Intent.EXTRA_INTENT); + if (intent != null) { + try { + int flagMask = 0; + int flagValues = 0; + if (activity.getIntent().getBooleanExtra( + Intent.EXTRA_RESULT_NEEDED, false)) { + flagMask = Intent.FLAG_ACTIVITY_FORWARD_RESULT; + flagValues = Intent.FLAG_ACTIVITY_FORWARD_RESULT; + } + activity.startIntentSenderForResult(intent, -1, null, + flagMask, flagValues, 0); + } catch (IntentSender.SendIntentException e) { + /* ignore */ + } + return; + } + } + RemoteCallback callback = activity.getIntent().getParcelableExtra( + Intent.EXTRA_REMOTE_CALLBACK); + if (callback != null) { + Bundle result = new Bundle(); + result.putBoolean(Intent.EXTRA_RETURN_RESULT, success); + callback.sendResult(result); + } + } + } +} diff --git a/src/com/android/packageinstaller/permission/ui/SecureButtonView.java b/src/com/android/packageinstaller/permission/ui/SecureButtonView.java new file mode 100644 index 00000000..624744e5 --- /dev/null +++ b/src/com/android/packageinstaller/permission/ui/SecureButtonView.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.packageinstaller.permission.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.Button; + +/** + * Extension of Button that uses the hidden MotionEvent flag for partially obscured windows to + * prevent tapjacking attacks. + */ +public class SecureButtonView extends Button { + + public SecureButtonView(Context context) { + this(context, null); + } + + public SecureButtonView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SecureButtonView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public SecureButtonView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public boolean onFilterTouchEventForSecurity(MotionEvent event) { + if ((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0 + || (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED) != 0) { + // Window is obscured, drop this touch. + return false; + } + return true; + } +} diff --git a/src/com/android/packageinstaller/permission/ui/handheld/AllAppPermissionsFragment.java b/src/com/android/packageinstaller/permission/ui/handheld/AllAppPermissionsFragment.java index b3b0895c..0c249e55 100644 --- a/src/com/android/packageinstaller/permission/ui/handheld/AllAppPermissionsFragment.java +++ b/src/com/android/packageinstaller/permission/ui/handheld/AllAppPermissionsFragment.java @@ -123,7 +123,7 @@ public final class AllAppPermissionsFragment extends SettingsWithHeader { } if ((perm.flags & PermissionInfo.FLAG_INSTALLED) == 0 - || (perm.flags & PermissionInfo.FLAG_HIDDEN) != 0) { + || (perm.flags & PermissionInfo.FLAG_REMOVED) != 0) { continue; } diff --git a/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionsFragment.java b/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionsFragment.java index f56cba70..422fb124 100644 --- a/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionsFragment.java +++ b/src/com/android/packageinstaller/permission/ui/handheld/AppPermissionsFragment.java @@ -16,7 +16,6 @@ package com.android.packageinstaller.permission.ui.handheld; -import android.annotation.Nullable; import android.app.ActionBar; import android.app.Activity; import android.app.AlertDialog; @@ -49,14 +48,17 @@ 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 com.android.settingslib.HelpUtils; +import com.android.settingslib.RestrictedLockUtils; import java.util.ArrayList; import java.util.List; +import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; + public final class AppPermissionsFragment extends SettingsWithHeader implements OnPreferenceChangeListener { @@ -115,6 +117,7 @@ public final class AppPermissionsFragment extends SettingsWithHeader public void onResume() { super.onResume(); mAppPermissions.refresh(); + loadPreferences(); setPreferencesCheckedState(); } @@ -140,7 +143,7 @@ public final class AppPermissionsFragment extends SettingsWithHeader } @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (mAppPermissions != null) { bindUi(this, mAppPermissions.getPackageInfo()); @@ -151,6 +154,8 @@ public final class AppPermissionsFragment extends SettingsWithHeader public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); menu.add(Menu.NONE, MENU_ALL_PERMS, Menu.NONE, R.string.all_permissions); + HelpUtils.prepareHelpMenuItem(getActivity(), menu, R.string.help_app_permissions, + getClass().getName()); } private static void bindUi(SettingsWithHeader fragment, PackageInfo packageInfo) { @@ -171,20 +176,6 @@ public final class AppPermissionsFragment extends SettingsWithHeader 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() { @@ -216,7 +207,7 @@ public final class AppPermissionsFragment extends SettingsWithHeader boolean isPlatform = group.getDeclaringPackage().equals(Utils.OS_PKG); - SwitchPreference preference = new SwitchPreference(context); + RestrictedSwitchPreference preference = new RestrictedSwitchPreference(context); preference.setOnPreferenceChangeListener(this); preference.setKey(group.getName()); Drawable icon = Utils.loadDrawable(context.getPackageManager(), @@ -225,10 +216,17 @@ public final class AppPermissionsFragment extends SettingsWithHeader android.R.attr.colorControlNormal)); preference.setTitle(group.getLabel()); if (group.isPolicyFixed()) { - preference.setSummary(getString(R.string.permission_summary_enforced_by_policy)); + EnforcedAdmin admin = RestrictedLockUtils.getProfileOrDeviceOwner(getContext(), + group.getUserId()); + if (admin != null) { + preference.setDisabledByAdmin(admin); + preference.setSummary(R.string.disabled_by_admin_summary_text); + } else { + preference.setSummary(R.string.permission_summary_enforced_by_policy); + preference.setEnabled(false); + } } preference.setPersistent(false); - preference.setEnabled(!group.isPolicyFixed()); preference.setChecked(group.areRuntimePermissionsGranted()); if (isPlatform) { @@ -273,12 +271,6 @@ public final class AppPermissionsFragment extends SettingsWithHeader return false; } - OverlayTouchActivity activity = (OverlayTouchActivity) getActivity(); - if (activity.isObscuredTouch()) { - activity.showOverlayDialog(); - return false; - } - addToggledGroup(group); if (LocationUtils.isLocationGroupAndProvider(group.getName(), group.getApp().packageName)) { @@ -294,7 +286,7 @@ public final class AppPermissionsFragment extends SettingsWithHeader .setMessage(grantedByDefault ? R.string.system_warning : R.string.old_sdk_deny_warning) .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.grant_dialog_button_deny, + .setPositiveButton(R.string.grant_dialog_button_deny_anyway, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -385,7 +377,7 @@ public final class AppPermissionsFragment extends SettingsWithHeader } @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); String packageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME); bindUi(this, getPackageInfo(getActivity(), packageName)); diff --git a/src/com/android/packageinstaller/permission/ui/handheld/GrantPermissionsViewHandlerImpl.java b/src/com/android/packageinstaller/permission/ui/handheld/GrantPermissionsViewHandlerImpl.java index 2d27f069..f2b0912d 100644 --- a/src/com/android/packageinstaller/permission/ui/handheld/GrantPermissionsViewHandlerImpl.java +++ b/src/com/android/packageinstaller/permission/ui/handheld/GrantPermissionsViewHandlerImpl.java @@ -16,23 +16,14 @@ package com.android.packageinstaller.permission.ui.handheld; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; import android.content.Context; -import android.content.res.Resources; import android.graphics.drawable.Icon; import android.os.Bundle; -import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; -import android.view.View.OnLayoutChangeListener; import android.view.ViewGroup; -import android.view.ViewParent; -import android.view.ViewRootImpl; import android.view.WindowManager.LayoutParams; -import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.Button; @@ -45,8 +36,6 @@ 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; - public final class GrantPermissionsViewHandlerImpl implements GrantPermissionsViewHandler, OnClickListener { @@ -59,14 +48,8 @@ public final class GrantPermissionsViewHandlerImpl public static final String ARG_GROUP_DO_NOT_ASK_CHECKED = "ARG_GROUP_DO_NOT_ASK_CHECKED"; // Animation parameters. - private static final long SIZE_START_DELAY = 300; - private static final long SIZE_START_LENGTH = 233; - private static final long FADE_OUT_START_DELAY = 300; - private static final long FADE_OUT_START_LENGTH = 217; - private static final long TRANSLATE_START_DELAY = 367; - private static final long TRANSLATE_LENGTH = 317; - private static final long GROUP_UPDATE_DELAY = 400; - private static final long DO_NOT_ASK_CHECK_DELAY = 450; + private static final long OUT_DURATION = 200; + private static final long IN_DURATION = 300; private final Context mContext; @@ -86,22 +69,13 @@ public final class GrantPermissionsViewHandlerImpl private CheckBox mDoNotAskCheckbox; private Button mAllowButton; - private ArrayList<ViewHeightController> mHeightControllers; private ManualLayoutFrame mRootView; // Needed for animation private ViewGroup mDescContainer; private ViewGroup mCurrentDesc; - private ViewGroup mNextDesc; - private ViewGroup mDialogContainer; - - private final Runnable mUpdateGroup = new Runnable() { - @Override - public void run() { - updateGroup(); - } - }; + private ButtonBarLayout mButtonBar; public GrantPermissionsViewHandlerImpl(Context context) { mContext = context; @@ -160,171 +134,148 @@ public final class GrantPermissionsViewHandlerImpl } } - private void animateToPermission() { - if (mHeightControllers == null) { - // We need to manually control the height of any views heigher than the root that - // we inflate. Find all the views up to the root and create ViewHeightControllers for - // them. - mHeightControllers = new ArrayList<>(); - ViewRootImpl viewRoot = mRootView.getViewRootImpl(); - ViewParent v = mRootView.getParent(); - addHeightController(mDialogContainer); - addHeightController(mRootView); - while (v != viewRoot) { - addHeightController((View) v); - v = v.getParent(); - } - // On the heighest level view, we want to setTop rather than setBottom to control the - // height, this way the dialog will grow up rather than down. - ViewHeightController realRootView = - mHeightControllers.get(mHeightControllers.size() - 1); - realRootView.setControlTop(true); - } - - // Grab the current height/y positions, then wait for the layout to change, - // so we can get the end height/y positions. - final SparseArray<Float> startPositions = getViewPositions(); - final int startHeight = mRootView.getLayoutHeight(); - mRootView.addOnLayoutChangeListener(new OnLayoutChangeListener() { - @Override - public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, - int oldTop, int oldRight, int oldBottom) { - mRootView.removeOnLayoutChangeListener(this); - SparseArray<Float> endPositions = getViewPositions(); - int endHeight = mRootView.getLayoutHeight(); - if (startPositions.get(R.id.do_not_ask_checkbox) == 0 - && endPositions.get(R.id.do_not_ask_checkbox) != 0) { - // If the checkbox didn't have a position before but has one now then set - // the start position to the end position because it just became visible. - startPositions.put(R.id.do_not_ask_checkbox, - endPositions.get(R.id.do_not_ask_checkbox)); - } - animateYPos(startPositions, endPositions, endHeight - startHeight); - } - }); + public void onConfigurationChanged() { + mRootView.onConfigurationChanged(); + } + private void animateOldContent(Runnable callback) { // Fade out old description group and scale out the icon for it. Interpolator interpolator = AnimationUtils.loadInterpolator(mContext, android.R.interpolator.fast_out_linear_in); + + // Icon scale to zero mIconView.animate() .scaleX(0) .scaleY(0) - .setStartDelay(FADE_OUT_START_DELAY) - .setDuration(FADE_OUT_START_LENGTH) + .setDuration(OUT_DURATION) .setInterpolator(interpolator) .start(); + + // Description fade out mCurrentDesc.animate() .alpha(0) - .setStartDelay(FADE_OUT_START_DELAY) - .setDuration(FADE_OUT_START_LENGTH) + .setDuration(OUT_DURATION) .setInterpolator(interpolator) - .setListener(null) + .withEndAction(callback) .start(); - // Update the index of the permission after the animations have started. - mCurrentGroupView.getHandler().postDelayed(mUpdateGroup, GROUP_UPDATE_DELAY); + // Checkbox fade out if needed + if (!mShowDonNotAsk && mDoNotAskCheckbox.getVisibility() == View.VISIBLE) { + mDoNotAskCheckbox.animate() + .alpha(0) + .setDuration(OUT_DURATION) + .setInterpolator(interpolator) + .start(); + } + } - // Add the new description and translate it in. - mNextDesc = (ViewGroup) LayoutInflater.from(mContext).inflate( + private void attachNewContent(final Runnable callback) { + mCurrentDesc = (ViewGroup) LayoutInflater.from(mContext).inflate( R.layout.permission_description, mDescContainer, false); + mDescContainer.removeAllViews(); + mDescContainer.addView(mCurrentDesc); + + mDialogContainer.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + mDialogContainer.removeOnLayoutChangeListener(this); + + // Prepare new content to the right to be moved in + final int containerWidth = mDescContainer.getWidth(); + mCurrentDesc.setTranslationX(containerWidth); + + // How much scale for the dialog to appear the same? + final int oldDynamicHeight = oldBottom - oldTop - mButtonBar.getHeight(); + final float scaleY = (float) oldDynamicHeight / mDescContainer.getHeight(); + + // How much to translate for the dialog to appear the same? + final int translationCompensatingScale = (int) (scaleY + * mDescContainer.getHeight() - mDescContainer.getHeight()) / 2; + final int translationY = (oldTop - top) + translationCompensatingScale; + + // Animate to the current layout + mDescContainer.setScaleY(scaleY); + mDescContainer.setTranslationY(translationY); + mDescContainer.animate() + .translationY(0) + .scaleY(1.0f) + .setInterpolator(AnimationUtils.loadInterpolator(mContext, + android.R.interpolator.linear_out_slow_in)) + .setDuration(IN_DURATION) + .withEndAction(callback) + .start(); + } + } + ); - mMessageView = (TextView) mNextDesc.findViewById(R.id.permission_message); - mIconView = (ImageView) mNextDesc.findViewById(R.id.permission_icon); - updateDescription(); - - int width = mDescContainer.getRootView().getWidth(); - mDescContainer.addView(mNextDesc); - mNextDesc.setTranslationX(width); - - final View oldDesc = mCurrentDesc; - // Remove the old view from the description, so that we can shrink if necessary. - mDescContainer.removeView(oldDesc); - oldDesc.setPadding(mDescContainer.getLeft(), mDescContainer.getTop(), - mRootView.getRight() - mDescContainer.getRight(), 0); - mRootView.addView(oldDesc); + mMessageView = (TextView) mCurrentDesc.findViewById(R.id.permission_message); + mIconView = (ImageView) mCurrentDesc.findViewById(R.id.permission_icon); - mCurrentDesc = mNextDesc; - mNextDesc.animate() - .translationX(0) - .setStartDelay(TRANSLATE_START_DELAY) - .setDuration(TRANSLATE_LENGTH) - .setInterpolator(AnimationUtils.loadInterpolator(mContext, - android.R.interpolator.linear_out_slow_in)) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - // This is the longest animation, when it finishes, we are done. - mRootView.removeView(oldDesc); - } - }) - .start(); + final boolean doNotAskWasShown = mDoNotAskCheckbox.getVisibility() == View.VISIBLE; - boolean visibleBefore = mDoNotAskCheckbox.getVisibility() == View.VISIBLE; + updateDescription(); + updateGroup(); updateDoNotAskCheckBox(); - boolean visibleAfter = mDoNotAskCheckbox.getVisibility() == View.VISIBLE; - if (visibleBefore != visibleAfter) { - Animation anim = AnimationUtils.loadAnimation(mContext, - visibleAfter ? android.R.anim.fade_in : android.R.anim.fade_out); - anim.setStartOffset(visibleAfter ? DO_NOT_ASK_CHECK_DELAY : 0); - mDoNotAskCheckbox.startAnimation(anim); + + if (!doNotAskWasShown && mShowDonNotAsk) { + mDoNotAskCheckbox.setAlpha(0); } } - private void addHeightController(View v) { - ViewHeightController heightController = new ViewHeightController(v); - heightController.setHeight(v.getHeight()); - mHeightControllers.add(heightController); - } + private void animateNewContent() { + Interpolator interpolator = AnimationUtils.loadInterpolator(mContext, + android.R.interpolator.linear_out_slow_in); - private SparseArray<Float> getViewPositions() { - SparseArray<Float> locMap = new SparseArray<>(); - final int N = mDialogContainer.getChildCount(); - for (int i = 0; i < N; i++) { - View child = mDialogContainer.getChildAt(i); - if (child.getId() <= 0) { - // Only track views with ids. - continue; - } - locMap.put(child.getId(), child.getY()); + // Description slide in + mCurrentDesc.animate() + .translationX(0) + .setDuration(IN_DURATION) + .setInterpolator(interpolator) + .start(); + + // Checkbox fade in if needed + if (mShowDonNotAsk && mDoNotAskCheckbox.getVisibility() == View.VISIBLE + && mDoNotAskCheckbox.getAlpha() < 1.0f) { + mDoNotAskCheckbox.setAlpha(0); + mDoNotAskCheckbox.animate() + .alpha(1.0f) + .setDuration(IN_DURATION) + .setInterpolator(interpolator) + .start(); } - return locMap; } - private void animateYPos(SparseArray<Float> startPositions, SparseArray<Float> endPositions, - int heightDiff) { - final int N = startPositions.size(); - for (int i = 0; i < N; i++) { - int key = startPositions.keyAt(i); - float start = startPositions.get(key); - float end = endPositions.get(key); - if (start != end) { - final View child = mDialogContainer.findViewById(key); - child.setTranslationY(start - end); - child.animate() - .setStartDelay(SIZE_START_DELAY) - .setDuration(SIZE_START_LENGTH) - .translationY(0) - .start(); + private void animateToPermission() { + // Remove the old content + animateOldContent(new Runnable() { + @Override + public void run() { + // Add the new content + attachNewContent(new Runnable() { + @Override + public void run() { + // Animate the new content + animateNewContent(); + } + }); } - } - for (int i = 0; i < mHeightControllers.size(); i++) { - mHeightControllers.get(i).animateAddHeight(heightDiff); - } + }); } @Override public View createView() { mRootView = (ManualLayoutFrame) LayoutInflater.from(mContext) .inflate(R.layout.grant_permissions, null); - ((ButtonBarLayout) mRootView.findViewById(R.id.button_group)).setAllowStacking(true); - - mDialogContainer = (ViewGroup) mRootView.findViewById(R.id.dialog_container); + mButtonBar = (ButtonBarLayout) mRootView.findViewById(R.id.button_group); + mButtonBar.setAllowStacking(true); mMessageView = (TextView) mRootView.findViewById(R.id.permission_message); mIconView = (ImageView) mRootView.findViewById(R.id.permission_icon); mCurrentGroupView = (TextView) mRootView.findViewById(R.id.current_page_text); mDoNotAskCheckbox = (CheckBox) mRootView.findViewById(R.id.do_not_ask_checkbox); mAllowButton = (Button) mRootView.findViewById(R.id.permission_allow_button); + mDialogContainer = (ViewGroup) mRootView.findViewById(R.id.dialog_container); mDescContainer = (ViewGroup) mRootView.findViewById(R.id.desc_container); mCurrentDesc = (ViewGroup) mRootView.findViewById(R.id.perm_desc_root); @@ -402,61 +353,4 @@ public final class GrantPermissionsViewHandlerImpl mResultListener.onPermissionGrantResult(mGroupName, false, doNotAskAgain); } } - - /** - * Manually controls the height of a view through getBottom/setTop. Also listens - * for layout changes and sets the height again to be sure it doesn't change. - */ - private static final class ViewHeightController implements OnLayoutChangeListener { - private final View mView; - private int mHeight; - private int mNextHeight; - private boolean mControlTop; - private ObjectAnimator mAnimator; - - public ViewHeightController(View view) { - mView = view; - mView.addOnLayoutChangeListener(this); - } - - public void setControlTop(boolean controlTop) { - mControlTop = controlTop; - } - - public void animateAddHeight(int heightDiff) { - if (heightDiff != 0) { - if (mNextHeight == 0) { - mNextHeight = mHeight; - } - mNextHeight += heightDiff; - if (mAnimator != null) { - mAnimator.cancel(); - } - mAnimator = ObjectAnimator.ofInt(this, "height", mHeight, mNextHeight); - mAnimator.setStartDelay(SIZE_START_DELAY); - mAnimator.setDuration(SIZE_START_LENGTH); - mAnimator.start(); - } - } - - public void setHeight(int height) { - mHeight = height; - updateHeight(); - } - - @Override - public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, - int oldTop, int oldRight, int oldBottom) { - // Ensure that the height never changes. - updateHeight(); - } - - private void updateHeight() { - if (mControlTop) { - mView.setTop(mView.getBottom() - mHeight); - } else { - mView.setBottom(mView.getTop() + mHeight); - } - } - } } diff --git a/src/com/android/packageinstaller/permission/ui/handheld/ManagePermissionsFragment.java b/src/com/android/packageinstaller/permission/ui/handheld/ManagePermissionsFragment.java index c53da879..238af36d 100644 --- a/src/com/android/packageinstaller/permission/ui/handheld/ManagePermissionsFragment.java +++ b/src/com/android/packageinstaller/permission/ui/handheld/ManagePermissionsFragment.java @@ -15,7 +15,6 @@ */ package com.android.packageinstaller.permission.ui.handheld; -import android.annotation.Nullable; import android.app.ActionBar; import android.app.FragmentTransaction; import android.content.ActivityNotFoundException; @@ -115,32 +114,15 @@ public final class ManagePermissionsFragment extends PermissionsFrameFragment } @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); bindPermissionUi(getActivity(), getView()); } - private static void bindPermissionUi(@Nullable Context context, @Nullable View rootView) { + private static void bindPermissionUi(Context context, 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() { @@ -260,7 +242,7 @@ public final class ManagePermissionsFragment extends PermissionsFrameFragment } @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + public void onViewCreated(View view, 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 index eee2f716..df0bdd46 100644 --- a/src/com/android/packageinstaller/permission/ui/handheld/PermissionAppsFragment.java +++ b/src/com/android/packageinstaller/permission/ui/handheld/PermissionAppsFragment.java @@ -43,14 +43,17 @@ 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 com.android.settingslib.HelpUtils; +import com.android.settingslib.RestrictedLockUtils; import java.util.ArrayList; import java.util.List; +import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; + public final class PermissionAppsFragment extends PermissionsFrameFragment implements Callback, Preference.OnPreferenceChangeListener { @@ -111,6 +114,8 @@ public final class PermissionAppsFragment extends PermissionsFrameFragment imple R.string.menu_show_system); mHideSystemMenu = menu.add(Menu.NONE, MENU_HIDE_SYSTEM, Menu.NONE, R.string.menu_hide_system); + HelpUtils.prepareHelpMenuItem(getActivity(), menu, R.string.help_app_permissions, + getClass().getName()); updateMenu(); } @@ -150,22 +155,6 @@ public final class PermissionAppsFragment extends PermissionsFrameFragment imple 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) { @@ -219,12 +208,20 @@ public final class PermissionAppsFragment extends PermissionsFrameFragment imple if (existingPref != null) { // If existing preference - only update its state. - if (app.isPolicyFixed()) { - existingPref.setSummary(getString( - R.string.permission_summary_enforced_by_policy)); + final boolean isPolicyFixed = app.isPolicyFixed(); + EnforcedAdmin enforcedAdmin = RestrictedLockUtils.getProfileOrDeviceOwner( + getActivity(), app.getUserId()); + if (!isTelevision && (existingPref instanceof RestrictedSwitchPreference)) { + ((RestrictedSwitchPreference) existingPref).setDisabledByAdmin( + isPolicyFixed ? enforcedAdmin : null); + existingPref.setSummary(isPolicyFixed ? + getString(R.string.disabled_by_admin_summary_text) : null); + } else { + existingPref.setEnabled(!isPolicyFixed); + existingPref.setSummary(isPolicyFixed ? + getString(R.string.permission_summary_enforced_by_policy) : null); } existingPref.setPersistent(false); - existingPref.setEnabled(!app.isPolicyFixed()); if (existingPref instanceof SwitchPreference) { ((SwitchPreference) existingPref) .setChecked(app.areRuntimePermissionsGranted()); @@ -232,16 +229,23 @@ public final class PermissionAppsFragment extends PermissionsFrameFragment imple continue; } - SwitchPreference pref = new SwitchPreference(context); + RestrictedSwitchPreference pref = new RestrictedSwitchPreference(context); pref.setOnPreferenceChangeListener(this); pref.setKey(app.getKey()); pref.setIcon(app.getIcon()); pref.setTitle(app.getLabel()); + EnforcedAdmin enforcedAdmin = RestrictedLockUtils.getProfileOrDeviceOwner( + getActivity(), app.getUserId()); if (app.isPolicyFixed()) { - pref.setSummary(getString(R.string.permission_summary_enforced_by_policy)); + if (!isTelevision && enforcedAdmin != null) { + pref.setDisabledByAdmin(enforcedAdmin); + pref.setSummary(R.string.disabled_by_admin_summary_text); + } else { + pref.setEnabled(false); + pref.setSummary(R.string.permission_summary_enforced_by_policy); + } } pref.setPersistent(false); - pref.setEnabled(!app.isPolicyFixed()); pref.setChecked(app.areRuntimePermissionsGranted()); if (isSystemApp && isTelevision) { @@ -318,12 +322,6 @@ public final class PermissionAppsFragment extends PermissionsFrameFragment imple return false; } - OverlayTouchActivity activity = (OverlayTouchActivity) getActivity(); - if (activity.isObscuredTouch()) { - activity.showOverlayDialog(); - return false; - } - addToggledGroup(app.getPackageName(), app.getPermissionGroup()); if (LocationUtils.isLocationGroupAndProvider(mPermissionApps.getGroupName(), @@ -340,7 +338,7 @@ public final class PermissionAppsFragment extends PermissionsFrameFragment imple .setMessage(grantedByDefault ? R.string.system_warning : R.string.old_sdk_deny_warning) .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.grant_dialog_button_deny, + .setPositiveButton(R.string.grant_dialog_button_deny_anyway, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { diff --git a/src/com/android/packageinstaller/permission/ui/handheld/RestrictedSwitchPreference.java b/src/com/android/packageinstaller/permission/ui/handheld/RestrictedSwitchPreference.java new file mode 100644 index 00000000..44a7f471 --- /dev/null +++ b/src/com/android/packageinstaller/permission/ui/handheld/RestrictedSwitchPreference.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.packageinstaller.permission.ui.handheld; + +import android.content.Context; +import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TextView; + +import com.android.packageinstaller.R; +import com.android.settingslib.RestrictedLockUtils; + +import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; + +public class RestrictedSwitchPreference extends SwitchPreference { + private final Context mContext; + private boolean mDisabledByAdmin; + private EnforcedAdmin mEnforcedAdmin; + private final int mSwitchWidgetResId; + + public RestrictedSwitchPreference(Context context) { + super(context); + mSwitchWidgetResId = getWidgetLayoutResource(); + mContext = context; + } + + @Override + public void onBindView(View view) { + super.onBindView(view); + if (mDisabledByAdmin) { + view.setEnabled(true); + } + if (mDisabledByAdmin) { + final TextView summaryView = (TextView) view.findViewById(android.R.id.summary); + if (summaryView != null) { + summaryView.setText( + isChecked() ? R.string.enabled_by_admin : R.string.disabled_by_admin); + summaryView.setVisibility(View.VISIBLE); + } + } + } + + @Override + public void setEnabled(boolean enabled) { + if (enabled && mDisabledByAdmin) { + setDisabledByAdmin(null); + } else { + super.setEnabled(enabled); + } + } + + public void setDisabledByAdmin(EnforcedAdmin admin) { + final boolean disabled = (admin != null ? true : false); + mEnforcedAdmin = admin; + if (mDisabledByAdmin != disabled) { + mDisabledByAdmin = disabled; + setWidgetLayoutResource(disabled ? R.layout.restricted_icon : mSwitchWidgetResId); + setEnabled(!disabled); + } + } + + @Override + public void performClick(PreferenceScreen preferenceScreen) { + if (mDisabledByAdmin) { + RestrictedLockUtils.sendShowAdminSupportDetailsIntent(mContext, mEnforcedAdmin); + } else { + super.performClick(preferenceScreen); + } + } +}
\ No newline at end of file diff --git a/src/com/android/packageinstaller/permission/ui/handheld/SettingsWithHeader.java b/src/com/android/packageinstaller/permission/ui/handheld/SettingsWithHeader.java index c15a4287..d5775796 100644 --- a/src/com/android/packageinstaller/permission/ui/handheld/SettingsWithHeader.java +++ b/src/com/android/packageinstaller/permission/ui/handheld/SettingsWithHeader.java @@ -28,7 +28,6 @@ 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 { diff --git a/src/com/android/packageinstaller/permission/ui/television/AllAppPermissionsFragment.java b/src/com/android/packageinstaller/permission/ui/television/AllAppPermissionsFragment.java index d4910128..0f8cb5b1 100644 --- a/src/com/android/packageinstaller/permission/ui/television/AllAppPermissionsFragment.java +++ b/src/com/android/packageinstaller/permission/ui/television/AllAppPermissionsFragment.java @@ -16,8 +16,10 @@ package com.android.packageinstaller.permission.ui.television; +import android.Manifest; import android.app.ActionBar; import android.app.AlertDialog; +import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; @@ -28,9 +30,12 @@ import android.content.pm.PermissionGroupInfo; import android.content.pm.PermissionInfo; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.provider.Settings; +import android.support.v14.preference.SwitchPreference; import android.support.v7.preference.Preference; +import android.support.v7.preference.Preference.OnPreferenceChangeListener; import android.support.v7.preference.Preference.OnPreferenceClickListener; import android.support.v7.preference.PreferenceCategory; import android.support.v7.preference.PreferenceGroup; @@ -38,6 +43,8 @@ import android.util.Log; import android.view.MenuItem; import com.android.packageinstaller.R; +import com.android.packageinstaller.permission.model.AppPermissionGroup; +import com.android.packageinstaller.permission.model.AppPermissions; import com.android.packageinstaller.permission.utils.Utils; import java.util.ArrayList; @@ -50,6 +57,10 @@ public final class AllAppPermissionsFragment extends SettingsWithHeader { private static final String KEY_OTHER = "other_perms"; + private PackageInfo mPackageInfo; + + private AppPermissions mAppPermissions; + public static AllAppPermissionsFragment newInstance(String packageName) { AllAppPermissionsFragment instance = new AllAppPermissionsFragment(); Bundle arguments = new Bundle(); @@ -67,6 +78,22 @@ public final class AllAppPermissionsFragment extends SettingsWithHeader { ab.setTitle(R.string.all_permissions); ab.setDisplayHomeAsUpEnabled(true); } + + String pkg = getArguments().getString(Intent.EXTRA_PACKAGE_NAME); + try { + mPackageInfo = getActivity().getPackageManager().getPackageInfo(pkg, + PackageManager.GET_PERMISSIONS); + } catch (NameNotFoundException e) { + getActivity().finish(); + } + + mAppPermissions = new AppPermissions(getActivity(), mPackageInfo, null, false, + new Runnable() { + @Override + public void run() { + getActivity().finish(); + } + }); } @Override @@ -86,62 +113,64 @@ public final class AllAppPermissionsFragment extends SettingsWithHeader { return super.onOptionsItemSelected(item); } - private void updateUi() { - if (getPreferenceScreen() != null) { - getPreferenceScreen().removeAll(); - } - addPreferencesFromResource(R.xml.all_permissions); + private PreferenceGroup getOtherGroup() { PreferenceGroup otherGroup = (PreferenceGroup) findPreference(KEY_OTHER); + if (otherGroup == null) { + otherGroup = new PreferenceCategory(getPreferenceManager().getContext()); + otherGroup.setKey(KEY_OTHER); + otherGroup.setTitle(getString(R.string.other_permissions)); + getPreferenceScreen().addPreference(otherGroup); + } + return otherGroup; + } + + private void updateUi() { + getPreferenceScreen().removeAll(); + 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(); + PackageManager pm = getActivity().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; - } + ApplicationInfo appInfo = mPackageInfo.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", mPackageInfo.packageName, null)); + } + setHeader(icon, label, infoIntent, null); - if ((perm.flags & PermissionInfo.FLAG_INSTALLED) == 0 - || (perm.flags & PermissionInfo.FLAG_HIDDEN) != 0) { - continue; - } + if (mPackageInfo.requestedPermissions != null) { + for (int i = 0; i < mPackageInfo.requestedPermissions.length; i++) { + PermissionInfo perm; + try { + perm = pm.getPermissionInfo(mPackageInfo.requestedPermissions[i], 0); + } catch (NameNotFoundException e) { + Log.e(LOG_TAG, "Can't get permission info for " + + mPackageInfo.requestedPermissions[i], e); + continue; + } + + if ((perm.flags & PermissionInfo.FLAG_INSTALLED) == 0 + || (perm.flags & PermissionInfo.FLAG_REMOVED) != 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)); + PermissionGroupInfo group = getGroup(perm.group, pm); + if (perm.protectionLevel == PermissionInfo.PROTECTION_DANGEROUS) { + PreferenceGroup pref = findOrCreate(group != null ? group : perm, pm, prefs); + pref.addPreference(getPreference(perm, group)); + } else if (perm.protectionLevel == PermissionInfo.PROTECTION_NORMAL) { + PreferenceGroup otherGroup = getOtherGroup(); + if (prefs.indexOf(otherGroup) < 0) { + prefs.add(otherGroup); } + getOtherGroup().addPreference(getPreference(perm, group)); } } - } 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 @@ -176,7 +205,7 @@ public final class AllAppPermissionsFragment extends SettingsWithHeader { ArrayList<Preference> prefs) { PreferenceGroup pref = (PreferenceGroup) findPreference(group.name); if (pref == null) { - pref = new PreferenceCategory(getContext()); + pref = new PreferenceCategory(getActivity()); pref.setKey(group.name); pref.setLayoutResource(R.layout.preference_category_material); pref.setTitle(group.loadLabel(pm)); @@ -186,26 +215,57 @@ public final class AllAppPermissionsFragment extends SettingsWithHeader { return pref; } - private Preference getPreference(PermissionInfo perm, PermissionGroupInfo group, - PackageManager pm) { - Preference pref = new Preference(getContext()); - pref.setLayoutResource(R.layout.preference_permissions); - Drawable icon = null; - if (perm.icon != 0) { - icon = perm.loadIcon(pm); - } else if (group != null && group.icon != 0) { - icon = group.loadIcon(pm); + private Preference getPreference(final PermissionInfo perm, final PermissionGroupInfo group) { + if (isMutableGranularPermission(perm.name)) { + return getMutablePreference(perm, group); } else { - icon = getContext().getDrawable(R.drawable.ic_perm_device_info); + return getImmutablePreference(perm, group); } - pref.setIcon(Utils.applyTint(getContext(), icon, android.R.attr.colorControlNormal)); + } + + private Preference getMutablePreference(final PermissionInfo perm, PermissionGroupInfo group) { + final AppPermissionGroup permGroup = mAppPermissions.getPermissionGroup(group.name); + final String[] filterPermissions = new String[]{perm.name}; + + // TODO: No hardcoded layouts + SwitchPreference pref = new SwitchPreference(getPreferenceManager().getContext()); + pref.setLayoutResource(R.layout.preference_permissions); + pref.setChecked(permGroup.areRuntimePermissionsGranted(filterPermissions)); + pref.setIcon(getTintedPermissionIcon(getActivity(), perm, group)); + pref.setTitle(perm.loadLabel(getActivity().getPackageManager())); + pref.setPersistent(false); + + pref.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object value) { + if (value == Boolean.TRUE) { + permGroup.grantRuntimePermissions(false, filterPermissions); + } else { + permGroup.revokeRuntimePermissions(false, filterPermissions); + } + return true; + } + }); + + return pref; + } + + private Preference getImmutablePreference(final PermissionInfo perm, + PermissionGroupInfo group) { + final PackageManager pm = getActivity().getPackageManager(); + + // TODO: No hardcoded layouts + Preference pref = new Preference(getActivity()); + pref.setLayoutResource(R.layout.preference_permissions); + pref.setIcon(getTintedPermissionIcon(getActivity(), perm, group)); pref.setTitle(perm.loadLabel(pm)); - final CharSequence desc = perm.loadDescription(pm); + pref.setPersistent(false); + pref.setOnPreferenceClickListener(new OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { - new AlertDialog.Builder(getContext()) - .setMessage(desc) + new AlertDialog.Builder(getActivity()) + .setMessage(perm.loadDescription(pm)) .setPositiveButton(android.R.string.ok, null) .show(); return true; @@ -214,4 +274,33 @@ public final class AllAppPermissionsFragment extends SettingsWithHeader { return pref; } + + private static Drawable getTintedPermissionIcon(Context context, PermissionInfo perm, + PermissionGroupInfo group) { + final Drawable icon; + if (perm.icon != 0) { + icon = perm.loadIcon(context.getPackageManager()); + } else if (group != null && group.icon != 0) { + icon = group.loadIcon(context.getPackageManager()); + } else { + icon = context.getDrawable(R.drawable.ic_perm_device_info); + } + return Utils.applyTint(context, icon, android.R.attr.colorControlNormal); + } + + private static boolean isMutableGranularPermission(String name) { + if (!Build.PERMISSIONS_REVIEW_REQUIRED) { + return false; + } + switch (name) { + case Manifest.permission.READ_CONTACTS: + case Manifest.permission.WRITE_CONTACTS: + case Manifest.permission.READ_SMS: + case Manifest.permission.READ_CALL_LOG: + case Manifest.permission.CALL_PHONE: { + return true; + } + } + return false; + } }
\ No newline at end of file diff --git a/src/com/android/packageinstaller/permission/ui/television/AppPermissionsFragment.java b/src/com/android/packageinstaller/permission/ui/television/AppPermissionsFragment.java index 42a2661c..26467de9 100644 --- a/src/com/android/packageinstaller/permission/ui/television/AppPermissionsFragment.java +++ b/src/com/android/packageinstaller/permission/ui/television/AppPermissionsFragment.java @@ -16,7 +16,6 @@ package com.android.packageinstaller.permission.ui.television; -import android.annotation.Nullable; import android.app.ActionBar; import android.app.Activity; import android.app.AlertDialog; @@ -28,6 +27,7 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.graphics.Color; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; @@ -37,20 +37,22 @@ import android.support.v7.preference.Preference; import android.support.v7.preference.Preference.OnPreferenceChangeListener; import android.support.v7.preference.Preference.OnPreferenceClickListener; import android.support.v7.preference.PreferenceScreen; +import android.support.v7.preference.PreferenceViewHolder; +import android.support.v7.widget.RecyclerView; 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.ui.ReviewPermissionsActivity; import com.android.packageinstaller.permission.utils.LocationUtils; import com.android.packageinstaller.permission.utils.SafetyNetLogger; import com.android.packageinstaller.permission.utils.Utils; @@ -103,12 +105,22 @@ public final class AppPermissionsFragment extends SettingsWithHeader return; } + mAppPermissions = new AppPermissions(activity, packageInfo, null, true, new Runnable() { @Override public void run() { getActivity().finish(); } }); + + if (mAppPermissions.isReviewRequired()) { + Intent intent = new Intent(getActivity(), ReviewPermissionsActivity.class); + intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName); + startActivity(intent); + getActivity().finish(); + return; + } + loadPreferences(); } @@ -116,6 +128,7 @@ public final class AppPermissionsFragment extends SettingsWithHeader public void onResume() { super.onResume(); mAppPermissions.refresh(); + loadPreferences(); setPreferencesCheckedState(); } @@ -141,7 +154,7 @@ public final class AppPermissionsFragment extends SettingsWithHeader } @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (mAppPermissions != null) { bindUi(this, mAppPermissions.getPackageInfo()); @@ -166,26 +179,8 @@ public final class AppPermissionsFragment extends SettingsWithHeader 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); - } + fragment.setHeader(icon, label, infoIntent, fragment.getString( + R.string.app_permissions_decor_title)); } private void loadPreferences() { @@ -196,9 +191,11 @@ public final class AppPermissionsFragment extends SettingsWithHeader PreferenceScreen screen = getPreferenceScreen(); screen.removeAll(); + screen.addPreference(createHeaderLineTwoPreference(context)); if (mExtraScreen != null) { mExtraScreen.removeAll(); + mExtraScreen = null; } final Preference extraPerms = new Preference(context); @@ -232,6 +229,7 @@ public final class AppPermissionsFragment extends SettingsWithHeader } else { if (mExtraScreen == null) { mExtraScreen = getPreferenceManager().createPreferenceScreen(context); + mExtraScreen.addPreference(createHeaderLineTwoPreference(context)); } mExtraScreen.addPreference(preference); } @@ -260,6 +258,30 @@ public final class AppPermissionsFragment extends SettingsWithHeader setLoading(false /* loading */, true /* animate */); } + /** + * Creates a heading below decor_title and above the rest of the preferences. This heading + * displays the app name and banner icon. It's used in both system and additional permissions + * fragments for each app. The styling used is the same as a leanback preference with a + * customized background color + * @param context The context the preferences created on + * @return The preference header to be inserted as the first preference in the list. + */ + private Preference createHeaderLineTwoPreference(Context context) { + Preference headerLineTwo = new Preference(context) { + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + holder.itemView.setBackgroundColor( + getResources().getColor(R.color.lb_header_banner_color)); + } + }; + headerLineTwo.setKey(HEADER_PREFERENCE_KEY); + headerLineTwo.setSelectable(false); + headerLineTwo.setTitle(mLabel); + headerLineTwo.setIcon(mIcon); + return headerLineTwo; + } + @Override public boolean onPreferenceChange(final Preference preference, Object newValue) { String groupName = preference.getKey(); @@ -269,12 +291,6 @@ public final class AppPermissionsFragment extends SettingsWithHeader return false; } - OverlayTouchActivity activity = (OverlayTouchActivity) getActivity(); - if (activity.isObscuredTouch()) { - activity.showOverlayDialog(); - return false; - } - addToggledGroup(group); if (LocationUtils.isLocationGroupAndProvider(group.getName(), group.getApp().packageName)) { @@ -290,7 +306,7 @@ public final class AppPermissionsFragment extends SettingsWithHeader .setMessage(grantedByDefault ? R.string.system_warning : R.string.old_sdk_deny_warning) .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.grant_dialog_button_deny, + .setPositiveButton(R.string.grant_dialog_button_deny_anyway, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -375,7 +391,6 @@ public final class AppPermissionsFragment extends SettingsWithHeader public void onCreate(Bundle savedInstanceState) { mOuterFragment = (AppPermissionsFragment) getTargetFragment(); super.onCreate(savedInstanceState); - setHeader(mOuterFragment.mIcon, mOuterFragment.mLabel, mOuterFragment.mInfoIntent); setHasOptionsMenu(true); } @@ -385,12 +400,28 @@ public final class AppPermissionsFragment extends SettingsWithHeader } @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); String packageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME); bindUi(this, getPackageInfo(getActivity(), packageName)); } + 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, fragment.getString( + R.string.additional_permissions_decor_title)); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { diff --git a/src/com/android/packageinstaller/permission/ui/television/ManagePermissionsFragment.java b/src/com/android/packageinstaller/permission/ui/television/ManagePermissionsFragment.java index 47301f48..35f866de 100644 --- a/src/com/android/packageinstaller/permission/ui/television/ManagePermissionsFragment.java +++ b/src/com/android/packageinstaller/permission/ui/television/ManagePermissionsFragment.java @@ -21,7 +21,6 @@ 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.support.v7.preference.Preference; import android.support.v7.preference.Preference.OnPreferenceClickListener; @@ -30,8 +29,6 @@ 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; @@ -42,7 +39,7 @@ import com.android.packageinstaller.permission.utils.Utils; import java.util.List; -public final class ManagePermissionsFragment extends PermissionsFrameFragment +public final class ManagePermissionsFragment extends SettingsWithHeader implements PermissionGroups.PermissionsGroupsChangeCallback, OnPreferenceClickListener { private static final String LOG_TAG = "ManagePermissionsFragment"; @@ -115,32 +112,17 @@ public final class ManagePermissionsFragment extends PermissionsFrameFragment } @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - bindPermissionUi(getActivity(), getView()); + bindPermissionUi(this, getView()); } - private static void bindPermissionUi(@Nullable Context context, @Nullable View rootView) { - if (context == null || rootView == null) { + private static void bindPermissionUi(SettingsWithHeader fragment, @Nullable View rootView) { + if (fragment == 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); - } + fragment.setHeader(null, null, null, fragment.getString( + R.string.manage_permissions_decor_title)); } private void updatePermissionsUi() { @@ -227,7 +209,7 @@ public final class ManagePermissionsFragment extends PermissionsFrameFragment } } - public static class AdditionalPermissionsFragment extends PermissionsFrameFragment { + public static class AdditionalPermissionsFragment extends SettingsWithHeader { @Override public void onCreate(Bundle icicle) { setLoading(true /* loading */, false /* animate */); @@ -253,9 +235,17 @@ public final class ManagePermissionsFragment extends PermissionsFrameFragment } @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - bindPermissionUi(getActivity(), getView()); + bindPermissionUi(this, getView()); + } + + private static void bindPermissionUi(SettingsWithHeader fragment, @Nullable View rootView) { + if (fragment == null || rootView == null) { + return; + } + fragment.setHeader(null, null, null, + fragment.getString(R.string.additional_permissions_decor_title)); } @Override diff --git a/src/com/android/packageinstaller/permission/ui/television/PermissionAppsFragment.java b/src/com/android/packageinstaller/permission/ui/television/PermissionAppsFragment.java index 0f240bef..29839c14 100644 --- a/src/com/android/packageinstaller/permission/ui/television/PermissionAppsFragment.java +++ b/src/com/android/packageinstaller/permission/ui/television/PermissionAppsFragment.java @@ -15,6 +15,7 @@ */ package com.android.packageinstaller.permission.ui.television; +import android.annotation.Nullable; import android.app.ActionBar; import android.app.AlertDialog; import android.app.Fragment; @@ -45,7 +46,7 @@ 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.ui.ReviewPermissionsActivity; import com.android.packageinstaller.permission.utils.LocationUtils; import com.android.packageinstaller.permission.utils.SafetyNetLogger; import com.android.packageinstaller.permission.utils.Utils; @@ -53,7 +54,7 @@ import com.android.packageinstaller.permission.utils.Utils; import java.util.ArrayList; import java.util.List; -public final class PermissionAppsFragment extends PermissionsFrameFragment implements Callback, +public final class PermissionAppsFragment extends SettingsWithHeader implements Callback, OnPreferenceChangeListener { private static final int MENU_SHOW_SYSTEM = Menu.FIRST; @@ -150,29 +151,12 @@ public final class PermissionAppsFragment extends PermissionsFrameFragment imple bindUi(this, mPermissionApps); } - private static void bindUi(Fragment fragment, PermissionApps permissionApps) { + private static void bindUi(SettingsWithHeader 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); - } + fragment.setHeader(null, null, null, + fragment.getString(R.string.permission_apps_decor_title, label)); } private void setOnPermissionsLoadedListener(Callback callback) { @@ -271,7 +255,8 @@ public final class PermissionAppsFragment extends PermissionsFrameFragment imple @Override public boolean onPreferenceClick(Preference preference) { SystemAppsFragment frag = new SystemAppsFragment(); - setPermissionName(frag, getArguments().getString(Intent.EXTRA_PERMISSION_NAME)); + setPermissionName(frag, getArguments().getString( + Intent.EXTRA_PERMISSION_NAME)); frag.setTargetFragment(PermissionAppsFragment.this, 0); getFragmentManager().beginTransaction() .replace(android.R.id.content, frag) @@ -321,19 +306,21 @@ public final class PermissionAppsFragment extends PermissionsFrameFragment imple return false; } - OverlayTouchActivity activity = (OverlayTouchActivity) getActivity(); - if (activity.isObscuredTouch()) { - activity.showOverlayDialog(); + if (LocationUtils.isLocationGroupAndProvider(mPermissionApps.getGroupName(), + app.getPackageName())) { + LocationUtils.showLocationDialog(getContext(), app.getLabel()); return false; } addToggledGroup(app.getPackageName(), app.getPermissionGroup()); - if (LocationUtils.isLocationGroupAndProvider(mPermissionApps.getGroupName(), - app.getPackageName())) { - LocationUtils.showLocationDialog(getContext(), app.getLabel()); + if (app.isReviewRequired()) { + Intent intent = new Intent(getActivity(), ReviewPermissionsActivity.class); + intent.putExtra(Intent.EXTRA_PACKAGE_NAME, app.getPackageName()); + startActivity(intent); return false; } + if (newValue == Boolean.TRUE) { app.grantRuntimePermissions(); } else { @@ -343,7 +330,7 @@ public final class PermissionAppsFragment extends PermissionsFrameFragment imple .setMessage(grantedByDefault ? R.string.system_warning : R.string.old_sdk_deny_warning) .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.grant_dialog_button_deny, + .setPositiveButton(R.string.grant_dialog_button_deny_anyway, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -394,7 +381,7 @@ public final class PermissionAppsFragment extends PermissionsFrameFragment imple } } - public static class SystemAppsFragment extends PermissionsFrameFragment implements Callback { + public static class SystemAppsFragment extends SettingsWithHeader implements Callback { PermissionAppsFragment mOuterFragment; @Override @@ -421,6 +408,14 @@ public final class PermissionAppsFragment extends PermissionsFrameFragment imple bindUi(this, permissionApps); } + + private static void bindUi(SettingsWithHeader fragment, PermissionApps permissionApps) { + final CharSequence label = permissionApps.getLabel(); + fragment.setHeader(null, null, null, + fragment.getString(R.string.system_apps_decor_title, label)); + } + + @Override public void onPermissionsLoaded(PermissionApps permissionApps) { setPreferenceScreen(); diff --git a/src/com/android/packageinstaller/permission/ui/television/PermissionsFrameFragment.java b/src/com/android/packageinstaller/permission/ui/television/PermissionsFrameFragment.java index e81aee86..6119f98c 100644 --- a/src/com/android/packageinstaller/permission/ui/television/PermissionsFrameFragment.java +++ b/src/com/android/packageinstaller/permission/ui/television/PermissionsFrameFragment.java @@ -22,7 +22,6 @@ import android.support.v14.preference.PreferenceFragment; import android.support.v17.leanback.widget.VerticalGridView; import android.support.v7.preference.PreferenceScreen; import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.RecyclerView.AdapterDataObserver; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -30,19 +29,19 @@ import android.view.animation.Animation; 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; public abstract class PermissionsFrameFragment extends PreferenceFragment { - private static final float WINDOW_ALIGNMENT_OFFSET_PERCENT = 50; + // Key identifying the preference used on TV as the extra header in a permission fragment. + // This is to distinguish it from the rest of the preferences + protected static final String HEADER_PREFERENCE_KEY = "HeaderPreferenceKey"; private ViewGroup mPreferencesContainer; - // TV-specific instance variables - @Nullable private VerticalGridView mGridView; + // TV-specific instance variable + @Nullable private RecyclerView mGridView; private View mLoadingView; private ViewGroup mPrefsView; @@ -74,7 +73,7 @@ public abstract class PermissionsFrameFragment extends PreferenceFragment { } @Override - public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) { + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { PreferenceScreen preferences = getPreferenceScreen(); if (preferences == null) { preferences = getPreferenceManager().createPreferenceScreen(getActivity()); @@ -133,18 +132,13 @@ public abstract class PermissionsFrameFragment extends PreferenceFragment { @Override public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, - Bundle savedInstanceState) { - if (DeviceUtils.isTelevision(getContext())) { - mGridView = (VerticalGridView) inflater.inflate( - R.layout.leanback_preferences_list, parent, false); - mGridView.setWindowAlignmentOffset(0); - mGridView.setWindowAlignmentOffsetPercent(WINDOW_ALIGNMENT_OFFSET_PERCENT); - mGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); - mGridView.setFocusScrollStrategy(VerticalGridView.FOCUS_SCROLL_ALIGNED); - return mGridView; - } else { - return super.onCreateRecyclerView(inflater, parent, savedInstanceState); - } + Bundle savedInstanceState) { + VerticalGridView verticalGridView = (VerticalGridView) inflater.inflate( + R.layout.leanback_preferences_list, parent, false); + verticalGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE); + verticalGridView.setFocusScrollStrategy(VerticalGridView.FOCUS_SCROLL_ALIGNED); + mGridView = verticalGridView; + return mGridView; } @Override @@ -155,7 +149,7 @@ public abstract class PermissionsFrameFragment extends PreferenceFragment { final TextView emptyView = (TextView) getView().findViewById(R.id.no_permissions); onSetEmptyText(emptyView); final RecyclerView recyclerView = getListView(); - adapter.registerAdapterDataObserver(new AdapterDataObserver() { + adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { @Override public void onChanged() { checkEmpty(); @@ -172,18 +166,20 @@ public abstract class PermissionsFrameFragment extends PreferenceFragment { } private void checkEmpty() { - boolean isEmpty = adapter.getItemCount() == 0; + boolean isEmpty = isPreferenceListEmpty(); emptyView.setVisibility(isEmpty ? View.VISIBLE : View.GONE); - recyclerView.setVisibility(isEmpty ? View.GONE : View.VISIBLE); + recyclerView.setVisibility(isEmpty && adapter.getItemCount() == 0 ? + View.GONE : View.VISIBLE); if (!isEmpty && mGridView != null) { mGridView.requestFocus(); } } }); - boolean isEmpty = adapter.getItemCount() == 0; + boolean isEmpty = isPreferenceListEmpty(); emptyView.setVisibility(isEmpty ? View.VISIBLE : View.GONE); - recyclerView.setVisibility(isEmpty ? View.GONE : View.VISIBLE); + recyclerView.setVisibility(isEmpty && adapter.getItemCount() == 0 ? + View.GONE : View.VISIBLE); if (!isEmpty && mGridView != null) { mGridView.requestFocus(); } @@ -192,6 +188,13 @@ public abstract class PermissionsFrameFragment extends PreferenceFragment { return adapter; } + private boolean isPreferenceListEmpty() { + PreferenceScreen screen = getPreferenceScreen(); + return screen.getPreferenceCount() == 0 || ( + screen.getPreferenceCount() == 1 && + (screen.findPreference(HEADER_PREFERENCE_KEY) != null)); + } + /** * Hook for subclasses to change the default text of the empty view. * Base implementation leaves the default empty view text. @@ -201,4 +204,3 @@ public abstract class PermissionsFrameFragment extends PreferenceFragment { protected void onSetEmptyText(TextView textView) { } } - diff --git a/src/com/android/packageinstaller/permission/ui/television/SettingsWithHeader.java b/src/com/android/packageinstaller/permission/ui/television/SettingsWithHeader.java index 4dae629c..06f7c142 100644 --- a/src/com/android/packageinstaller/permission/ui/television/SettingsWithHeader.java +++ b/src/com/android/packageinstaller/permission/ui/television/SettingsWithHeader.java @@ -25,10 +25,8 @@ 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 { @@ -37,50 +35,40 @@ public abstract class SettingsWithHeader extends PermissionsFrameFragment protected Intent mInfoIntent; protected Drawable mIcon; protected CharSequence mLabel; + protected CharSequence mDecorTitle; @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(); - } + mHeader = inflater.inflate(R.layout.header, root, false); + getPreferencesContainer().addView(mHeader, 0); + updateHeader(); return root; } - public void setHeader(Drawable icon, CharSequence label, Intent infoIntent) { + public void setHeader(Drawable icon, CharSequence label, Intent infoIntent, + CharSequence decorTitle) { mIcon = icon; mLabel = label; mInfoIntent = infoIntent; + mDecorTitle = decorTitle; 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); + public View getHeader() { + return mHeader; + } - 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); - } - } + protected void updateHeader() { + final TextView decorTitle = (TextView) mHeader.findViewById(R.id.decor_title); + decorTitle.setText(mDecorTitle); } @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 index aba97fc8..db1c94d8 100644 --- a/src/com/android/packageinstaller/permission/ui/wear/AppPermissionsFragmentWear.java +++ b/src/com/android/packageinstaller/permission/ui/wear/AppPermissionsFragmentWear.java @@ -17,11 +17,9 @@ 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; @@ -35,7 +33,6 @@ 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; @@ -120,22 +117,22 @@ public final class AppPermissionsFragmentWear extends TitledSettingsFragment { } @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (mAppPermissions != null) { initializeLayout(mAdapter); - bindHeader(mAppPermissions.getPackageInfo()); + mHeader.setText(R.string.app_permissions); + mDetails.setText(R.string.no_permissions); + if (mAdapter.getItemCount() == 0) { + mDetails.setVisibility(View.VISIBLE); + mWheel.setVisibility(View.GONE); + } else { + mDetails.setVisibility(View.GONE); + mWheel.setVisibility(View.VISIBLE); + } } } - 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(); @@ -197,12 +194,6 @@ public final class AppPermissionsFragmentWear extends TitledSettingsFragment { return; } - OverlayTouchActivity activity = (OverlayTouchActivity) getActivity(); - if (activity.isObscuredTouch()) { - activity.showOverlayDialog(); - return; - } - addToggledGroup(group); if (LocationUtils.isLocationGroupAndProvider(group.getName(), group.getApp().packageName)) { diff --git a/src/com/android/packageinstaller/permission/ui/wear/TitledSettingsFragment.java b/src/com/android/packageinstaller/permission/ui/wear/TitledSettingsFragment.java index ef7efb28..b673a498 100644 --- a/src/com/android/packageinstaller/permission/ui/wear/TitledSettingsFragment.java +++ b/src/com/android/packageinstaller/permission/ui/wear/TitledSettingsFragment.java @@ -28,7 +28,7 @@ import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; -import android.widget.FrameLayout; +import android.widget.LinearLayout; import android.widget.TextView; import com.android.packageinstaller.permission.ui.wear.settings.ViewUtils; @@ -46,6 +46,7 @@ public abstract class TitledSettingsFragment extends Fragment implements private int mInitialHeaderHeight; protected TextView mHeader; + protected TextView mDetails; protected WearableListView mWheel; private int mCharLimitShortTitle; @@ -114,6 +115,9 @@ public abstract class TitledSettingsFragment extends Fragment implements mHeader.addOnLayoutChangeListener(this); mHeader.addTextChangedListener(mHeaderTextWatcher); + mDetails = (TextView) v.findViewById(R.id.details); + mDetails.addOnLayoutChangeListener(this); + mWheel.setAdapter(adapter); mWheel.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override @@ -134,13 +138,14 @@ public abstract class TitledSettingsFragment extends Fragment implements adjustHeaderSize(); - positionOnCircular(getContext(), mHeader, mWheel); + positionOnCircular(getContext(), mHeader, mDetails, mWheel); } - public void positionOnCircular(Context context, View header, final ViewGroup wheel) { + public void positionOnCircular(Context context, View header, View details, + final ViewGroup wheel) { if (ViewUtils.getIsCircular(context)) { - FrameLayout.LayoutParams params = - (FrameLayout.LayoutParams) header.getLayoutParams(); + LinearLayout.LayoutParams params = + (LinearLayout.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 @@ -152,10 +157,14 @@ public abstract class TitledSettingsFragment extends Fragment implements params.rightMargin = margin; params.gravity = Gravity.CENTER_HORIZONTAL; header.setLayoutParams(params); + details.setLayoutParams(params); if (header instanceof TextView) { ((TextView) header).setGravity(Gravity.CENTER); } + if (details instanceof TextView) { + ((TextView) details).setGravity(Gravity.CENTER); + } final int leftPadding = (int) context.getResources().getDimension( R.dimen.round_content_padding_left); @@ -209,7 +218,7 @@ public abstract class TitledSettingsFragment extends Fragment implements } mHeader.setMinHeight((int) height); - FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mHeader.getLayoutParams(); + LinearLayout.LayoutParams params = (LinearLayout.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. diff --git a/src/com/android/packageinstaller/permission/ui/wear/WarningConfirmationActivity.java b/src/com/android/packageinstaller/permission/ui/wear/WarningConfirmationActivity.java index 03713419..0800c14c 100644 --- a/src/com/android/packageinstaller/permission/ui/wear/WarningConfirmationActivity.java +++ b/src/com/android/packageinstaller/permission/ui/wear/WarningConfirmationActivity.java @@ -66,7 +66,7 @@ public final class WarningConfirmationActivity extends Activity { @Override public CharSequence getVerticalButton2Text() { - return getString(R.string.grant_dialog_button_deny); + return getString(R.string.grant_dialog_button_deny_anyway); } @Override diff --git a/src/com/android/packageinstaller/permission/utils/ArrayUtils.java b/src/com/android/packageinstaller/permission/utils/ArrayUtils.java new file mode 100644 index 00000000..4b7a3947 --- /dev/null +++ b/src/com/android/packageinstaller/permission/utils/ArrayUtils.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.packageinstaller.permission.utils; + +import java.util.Objects; + +public final class ArrayUtils { + private ArrayUtils() { /* cannot be instantiated */ } + + /** + * Checks that value is present as at least one of the elements of the array. + * @param array the array to check in + * @param value the value to check for + * @return true if the value is present in the array + */ + public static <T> boolean contains(T[] array, T value) { + return indexOf(array, value) != -1; + } + + /** + * Return first index of {@code value} in {@code array}, or {@code -1} if + * not found. + */ + public static <T> int indexOf(T[] array, T value) { + if (array == null) return -1; + for (int i = 0; i < array.length; i++) { + if (Objects.equals(array[i], value)) return i; + } + return -1; + } +} diff --git a/src/com/android/packageinstaller/permission/utils/IoUtils.java b/src/com/android/packageinstaller/permission/utils/IoUtils.java new file mode 100644 index 00000000..ff7d1831 --- /dev/null +++ b/src/com/android/packageinstaller/permission/utils/IoUtils.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.packageinstaller.permission.utils; + +public final class IoUtils { + private IoUtils() { + } + + /** + * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null. + */ + public static void closeQuietly(AutoCloseable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } +} diff --git a/src/com/android/packageinstaller/permission/utils/Utils.java b/src/com/android/packageinstaller/permission/utils/Utils.java index 21830378..22663e13 100644 --- a/src/com/android/packageinstaller/permission/utils/Utils.java +++ b/src/com/android/packageinstaller/permission/utils/Utils.java @@ -22,7 +22,6 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; -import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.Resources.Theme; import android.graphics.drawable.Drawable; @@ -36,7 +35,7 @@ import com.android.packageinstaller.permission.model.PermissionApps.PermissionAp import java.util.List; -public class Utils { +public final class Utils { private static final String LOG_TAG = "Utils"; diff --git a/src/com/android/packageinstaller/wear/WearPackageArgs.java b/src/com/android/packageinstaller/wear/WearPackageArgs.java index 67051da0..da49192a 100644 --- a/src/com/android/packageinstaller/wear/WearPackageArgs.java +++ b/src/com/android/packageinstaller/wear/WearPackageArgs.java @@ -25,6 +25,8 @@ import android.os.Bundle; * installing/uninstalling. */ public class WearPackageArgs { + private static final String KEY_PACKAGE_NAME = + "com.google.android.clockwork.EXTRA_PACKAGE_NAME"; private static final String KEY_ASSET_URI = "com.google.android.clockwork.EXTRA_ASSET_URI"; private static final String KEY_START_ID = @@ -45,16 +47,16 @@ public class WearPackageArgs { "com.google.android.clockwork.EXTRA_KEY_SHOULD_CHECK_GMS_DEPENDENCY"; public static String getPackageName(Bundle b) { - return b.getString(Intent.EXTRA_INSTALLER_PACKAGE_NAME); + return b.getString(KEY_PACKAGE_NAME); } - public static Uri getAssetUri(Bundle b) { - return b.getParcelable(KEY_ASSET_URI); + public static Bundle setPackageName(Bundle b, String packageName) { + b.putString(KEY_PACKAGE_NAME, packageName); + return b; } - public static Bundle setAssetUri(Bundle b, Uri assetUri) { - b.putParcelable(KEY_ASSET_URI, assetUri); - return b; + public static Uri getAssetUri(Bundle b) { + return b.getParcelable(KEY_ASSET_URI); } public static Uri getPermUri(Bundle b) { diff --git a/src/com/android/packageinstaller/wear/WearPackageInstallerService.java b/src/com/android/packageinstaller/wear/WearPackageInstallerService.java index 3874c0a4..7387ed2a 100644 --- a/src/com/android/packageinstaller/wear/WearPackageInstallerService.java +++ b/src/com/android/packageinstaller/wear/WearPackageInstallerService.java @@ -66,13 +66,18 @@ import java.util.Set; * * Install Action example: * adb shell am startservice -a com.android.packageinstaller.wear.INSTALL_PACKAGE \ - * -t vnd.android.cursor.item/wearable_apk \ - * -d content://com.google.android.clockwork.home.provider/host/com.google.android.wearable.app/wearable/com.google.android.gms/apk \ + * -d package://com.google.android.gms \ + * --eu com.google.android.clockwork.EXTRA_ASSET_URI content://com.google.android.clockwork.home.provider/host/com.google.android.wearable.app/wearable/com.google.android.gms/apk \ * --es android.intent.extra.INSTALLER_PACKAGE_NAME com.google.android.gms \ * --ez com.google.android.clockwork.EXTRA_CHECK_PERMS false \ * --eu com.google.android.clockwork.EXTRA_PERM_URI content://com.google.android.clockwork.home.provider/host/com.google.android.wearable.app/permissions \ * com.android.packageinstaller/com.android.packageinstaller.wear.WearPackageInstallerService * + * Uninstall Action example: + * adb shell am startservice -a com.android.packageinstaller.wear.UNINSTALL_PACKAGE \ + * -d package://com.google.android.gms \ + * com.android.packageinstaller/com.android.packageinstaller.wear.WearPackageInstallerService + * * Retry GMS: * adb shell am startservice -a com.android.packageinstaller.wear.RETRY_GMS \ * com.android.packageinstaller/com.android.packageinstaller.wear.WearPackageInstallerService @@ -141,29 +146,49 @@ public class WearPackageInstallerService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { if (!DeviceUtils.isWear(this)) { - Log.w(TAG, "Not running on wearable"); + Log.w(TAG, "Not running on wearable."); + return START_NOT_STICKY; + } + + if (intent == null) { + Log.w(TAG, "Got null intent."); return START_NOT_STICKY; } + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Got install/uninstall request " + intent); + } + + Uri packageUri = intent.getData(); + if (packageUri == null) { + Log.e(TAG, "No package URI in intent"); + return START_NOT_STICKY; + } + final String packageName = WearPackageUtil.getSanitizedPackageName(packageUri); + if (packageName == null) { + Log.e(TAG, "Invalid package name in URI (expected package:<pkgName>): " + packageUri); + return START_NOT_STICKY; + } + PowerManager.WakeLock lock = getLock(this.getApplicationContext()); if (!lock.isHeld()) { lock.acquire(); } - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Got install/uninstall request " + intent); + + Bundle intentBundle = intent.getExtras(); + if (intentBundle == null) { + intentBundle = new Bundle(); } - if (intent != null) { - Bundle intentBundle = intent.getExtras(); - WearPackageArgs.setStartId(intentBundle, startId); - if (Intent.ACTION_INSTALL_PACKAGE.equals(intent.getAction())) { - final Message msg = mServiceHandler.obtainMessage(START_INSTALL); - WearPackageArgs.setAssetUri(intentBundle, intent.getData()); - msg.setData(intentBundle); - mServiceHandler.sendMessage(msg); - } else if (Intent.ACTION_UNINSTALL_PACKAGE.equals(intent.getAction())) { - Message msg = mServiceHandler.obtainMessage(START_UNINSTALL); - msg.setData(intentBundle); - mServiceHandler.sendMessage(msg); - } + WearPackageArgs.setStartId(intentBundle, startId); + WearPackageArgs.setPackageName(intentBundle, packageName); + if (Intent.ACTION_INSTALL_PACKAGE.equals(intent.getAction())) { + Message msg = mServiceHandler.obtainMessage(START_INSTALL); + msg.setData(intentBundle); + mServiceHandler.sendMessage(msg); + } else if (Intent.ACTION_UNINSTALL_PACKAGE.equals(intent.getAction())) { + Message msg = mServiceHandler.obtainMessage(START_UNINSTALL); + msg.setData(intentBundle); + mServiceHandler.sendMessage(msg); } return START_NOT_STICKY; } diff --git a/src/com/android/packageinstaller/wear/WearPackageUtil.java b/src/com/android/packageinstaller/wear/WearPackageUtil.java index 688d6167..e340d627 100644 --- a/src/com/android/packageinstaller/wear/WearPackageUtil.java +++ b/src/com/android/packageinstaller/wear/WearPackageUtil.java @@ -16,11 +16,11 @@ package com.android.packageinstaller.wear; -import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageParser; +import android.net.Uri; import android.os.ParcelFileDescriptor; import android.system.ErrnoException; import android.system.Os; @@ -86,7 +86,7 @@ public class WearPackageUtil { * decompress it here */ public static File getFileFromFd(Context context, ParcelFileDescriptor fd, - String packageName, @Nullable String compressionAlg) { + String packageName, String compressionAlg) { File newFile = getTemporaryFile(context, packageName); if (fd == null || fd.getFileDescriptor() == null) { return null; @@ -164,4 +164,16 @@ public class WearPackageUtil { + " for " + wearablePackageName); context.startService(newIntent); } + + /** + * @return com.google.com from expected formats like + * Uri: package:com.google.com, package:/com.google.com, package://com.google.com + */ + public static String getSanitizedPackageName(Uri packageUri) { + String packageName = packageUri.getEncodedSchemeSpecificPart(); + if (packageName != null) { + return packageName.replaceAll("^/+", ""); + } + return packageName; + } } |