Loading AndroidManifest.xml +10 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.KILL_UID" /> <uses-permission android:name="android.permission.MANAGE_APP_OPS_RESTRICTIONS" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="com.google.android.permission.INSTALL_WEARABLE_PACKAGES" /> Loading Loading @@ -63,6 +64,15 @@ android:theme="@style/DialogWhenLargeNoAnimation" android:exported="false" /> <receiver android:name=".InstallEventReceiver" android:permission="android.permission.INSTALL_PACKAGES" android:exported="true"> <intent-filter> <action android:name="com.android.packageinstaller.ACTION_INSTALL_COMMIT" /> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver> <activity android:name=".InstallSuccess" android:theme="@style/DialogWhenLargeNoAnimation" android:exported="false" /> Loading src/com/android/packageinstaller/EventResultPersister.java 0 → 100644 +315 −0 Original line number Diff line number Diff line /* * 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; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInstaller; import android.os.AsyncTask; import android.util.AtomicFile; import android.util.Log; import android.util.SparseArray; import android.util.Xml; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.FastXmlSerializer; import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; /** * Persists results of events and calls back observers when a matching result arrives. */ class EventResultPersister { private static final String LOG_TAG = EventResultPersister.class.getSimpleName(); /** Id passed to {@link #addObserver(int, EventResultObserver)} to generate new id */ static final int GENERATE_NEW_ID = Integer.MIN_VALUE; /** * The extra with the id to set in the intent delivered to * {@link #onEventReceived(Context, Intent)} */ static final String EXTRA_ID = "EventResultPersister.EXTRA_ID"; /** Persisted state of this object */ private final AtomicFile mResultsFile; private final Object mLock = new Object(); /** Currently stored but not yet called back results (install id -> status, status message) */ @GuardedBy("mLock") private final SparseArray<EventResult> mResults = new SparseArray<>(); /** Currently registered, not called back observers (install id -> observer) */ @GuardedBy("mLock") private final SparseArray<EventResultObserver> mObservers = new SparseArray<>(); /** Always increasing counter for install event ids */ @GuardedBy("mLock") private int mCounter; /** If a write that will persist the state is scheduled */ @GuardedBy("mLock") private boolean mIsPersistScheduled; /** If the state was changed while the data was being persisted */ @GuardedBy("mLock") private boolean mIsPersistingStateValid; /** Call back when a result is received. Observer is removed when onResult it called. */ interface EventResultObserver { void onResult(int status, @Nullable String message); } /** * Read persisted state. * * @param resultFile The file the results are persisted in */ EventResultPersister(@NonNull File resultFile) { mResultsFile = new AtomicFile(resultFile); mCounter = GENERATE_NEW_ID + 1; try (FileInputStream stream = mResultsFile.openRead()) { XmlPullParser parser = Xml.newPullParser(); parser.setInput(stream, StandardCharsets.UTF_8.name()); XmlUtils.nextElement(parser); while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { String tagName = parser.getName(); if ("results".equals(tagName)) { mCounter = XmlUtils.readIntAttribute(parser, "counter", GENERATE_NEW_ID + 1); } else if ("result".equals(tagName)) { int id = XmlUtils.readIntAttribute(parser, "id", 0); int status = XmlUtils.readIntAttribute(parser, "status", 0); String statusMessage = XmlUtils.readStringAttribute(parser, "statusMessage"); if (mResults.get(id) != null) { throw new Exception("id " + id + " has two results"); } mResults.put(id, new EventResult(status, statusMessage)); } else { throw new Exception("unexpected tag"); } XmlUtils.nextElement(parser); } } catch (Exception e) { mResults.clear(); writeState(); } } /** * Add a result. If the result is an pending user action, execute the pending user action * directly and do not queue a result. * * @param context The context the event was received in * @param intent The intent the activity received */ void onEventReceived(@NonNull Context context, @NonNull Intent intent) { int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0); if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) { context.startActivity(intent.getParcelableExtra(Intent.EXTRA_INTENT)); return; } int id = intent.getIntExtra(EXTRA_ID, 0); String statusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE); EventResultObserver observerToCall = null; synchronized (mLock) { int numObservers = mObservers.size(); for (int i = 0; i < numObservers; i++) { if (mObservers.keyAt(i) == id) { observerToCall = mObservers.valueAt(i); mObservers.removeAt(i); break; } } if (observerToCall != null) { observerToCall.onResult(status, statusMessage); } else { mResults.put(id, new EventResult(status, statusMessage)); writeState(); } } } /** * Persist current state. The persistence might be delayed. */ private void writeState() { synchronized (mLock) { mIsPersistingStateValid = false; if (!mIsPersistScheduled) { mIsPersistScheduled = true; AsyncTask.execute(() -> { int counter; SparseArray<EventResult> results; while (true) { // Take snapshot of state synchronized (mLock) { counter = mCounter; results = mResults.clone(); mIsPersistingStateValid = true; } FileOutputStream stream = null; try { stream = mResultsFile.startWrite(); FastXmlSerializer serializer = new FastXmlSerializer(); serializer.setOutput(stream, StandardCharsets.UTF_8.name()); serializer.startDocument(null, true); serializer.setFeature( "http://xmlpull.org/v1/doc/features.html#indent-output", true); serializer.startTag(null, "results"); serializer.attribute(null, "counter", Integer.toString(counter)); int numResults = results.size(); for (int i = 0; i < numResults; i++) { serializer.startTag(null, "result"); serializer.attribute(null, "id", Integer.toString(results.keyAt(i))); serializer.attribute(null, "status", Integer.toString(results.valueAt(i).status)); if (results.valueAt(i).message != null) { serializer.attribute(null, "statusMessage", results.valueAt(i).message); } serializer.endTag(null, "result"); } serializer.endTag(null, "results"); serializer.endDocument(); mResultsFile.finishWrite(stream); } catch (IOException e) { if (stream != null) { mResultsFile.failWrite(stream); } Log.e(LOG_TAG, "error writing results", e); mResultsFile.delete(); } // Check if there was changed state since we persisted. If so, we need to // persist again. synchronized (mLock) { if (mIsPersistingStateValid) { mIsPersistScheduled = false; break; } } } }); } } } /** * Add an observer. If there is already an event for this id, call back inside of this call. * * @param id The id the observer is for or {@code GENERATE_NEW_ID} to generate a new one. * @param observer The observer to call back. * * @return The id for this event */ int addObserver(int id, @NonNull EventResultObserver observer) throws OutOfIdsException { boolean stateChanged = false; synchronized (mLock) { int resultIndex = 0; EventResult result = null; if (id == GENERATE_NEW_ID) { if (mCounter == Integer.MAX_VALUE) { throw new OutOfIdsException(); } else { id = mCounter; mCounter++; stateChanged = true; } } else { resultIndex = mResults.indexOfKey(id); result = mResults.valueAt(resultIndex); } // Check if we can instantly call back if (result != null) { observer.onResult(result.status, result.message); mResults.removeAt(resultIndex); stateChanged = true; } else { mObservers.put(id, observer); } if (stateChanged) { writeState(); } } return id; } /** * Remove a observer. * * @param id The id the observer was added for */ void removeObserver(int id) { synchronized (mLock) { mObservers.delete(id); } } /** * The status from an event. */ private class EventResult { public final int status; @Nullable public final String message; private EventResult(int status, @Nullable String message) { this.status = status; this.message = message; } } class OutOfIdsException extends Exception {} } src/com/android/packageinstaller/InstallEventReceiver.java 0 → 100644 +85 −0 Original line number Diff line number Diff line /* * 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; import android.annotation.NonNull; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import java.io.File; /** * Receives install events and perists them using a {@link EventResultPersister}. */ public class InstallEventReceiver extends BroadcastReceiver { private static final Object sLock = new Object(); private static EventResultPersister sReceiver; /** * Get the event receiver persisting the results * * @return The event receiver. */ @NonNull private static EventResultPersister getReceiver(@NonNull Context context) { synchronized (sLock) { if (sReceiver == null) { sReceiver = new EventResultPersister(new File(context.getNoBackupFilesDir(), "install_results.xml")); } } return sReceiver; } @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) { // Do not persist installs over reboot synchronized (sLock) { new File(context.getNoBackupFilesDir(), "install_results.xml").delete(); } } else { getReceiver(context).onEventReceived(context, intent); } } /** * Add an observer. If there is already an event for this id, call back inside of this call. * * @param context A context of the current app * @param id The id the observer is for or {@code GENERATE_NEW_ID} to generate a new one. * @param observer The observer to call back. * * @return The id for this event */ static int addObserver(@NonNull Context context, int id, @NonNull EventResultPersister.EventResultObserver observer) throws EventResultPersister.OutOfIdsException { return getReceiver(context).addObserver(id, observer); } /** * Remove a observer. * * @param context A context of the current app * @param id The id the observer was added for */ static void removeObserver(@NonNull Context context, int id) { getReceiver(context).removeObserver(id); } } src/com/android/packageinstaller/InstallInstalling.java +48 −59 Original line number Diff line number Diff line Loading @@ -21,10 +21,7 @@ import static android.content.pm.PackageInstaller.SessionParams.UID_UNKNOWN; import android.annotation.Nullable; import android.app.Activity; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; Loading Loading @@ -54,16 +51,11 @@ public class InstallInstalling extends Activity { private static final String LOG_TAG = InstallInstalling.class.getSimpleName(); private static final String SESSION_ID = "com.android.packageinstaller.SESSION_ID"; private static final String INSTALL_ID = "com.android.packageinstaller.INSTALL_ID"; private static final String BROADCAST_ACTION = "com.android.packageinstaller.ACTION_INSTALL_COMMIT"; private static final String BROADCAST_SENDER_PERMISSION = "android.permission.INSTALL_PACKAGES"; /** Receiver receiving the results of the installation */ private BroadcastReceiver mBroadcastReceiver; /** Listens to changed to the session and updates progress bar */ private PackageInstaller.SessionCallback mSessionCallback; Loading @@ -73,12 +65,12 @@ public class InstallInstalling extends Activity { /** Id of the session to install the package */ private int mSessionId; /** Id of the install event we wait for */ private int mInstallId; /** URI of package to install */ private Uri mPackageURI; /** Info about the app to info */ private ApplicationInfo mAppInfo; /** The button that can cancel this dialog */ private Button mCancelButton; Loading @@ -88,23 +80,34 @@ public class InstallInstalling extends Activity { setContentView(R.layout.install_installing); mAppInfo = getIntent().getParcelableExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO); ApplicationInfo appInfo = getIntent() .getParcelableExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO); mPackageURI = getIntent().getData(); if ("package".equals(mPackageURI.getScheme())) { try { getPackageManager().installExistingPackage(mAppInfo.packageName); getPackageManager().installExistingPackage(appInfo.packageName); launchSuccess(); } catch (PackageManager.NameNotFoundException e) { launchFailure(PackageInstaller.STATUS_FAILURE_INVALID, null); } } else { final File sourceFile = new File(mPackageURI.getPath()); PackageUtil.initSnippetForNewApp(this, PackageUtil.getAppSnippet(this, mAppInfo, PackageUtil.initSnippetForNewApp(this, PackageUtil.getAppSnippet(this, appInfo, sourceFile), R.id.app_snippet); if (savedInstanceState != null) { mSessionId = savedInstanceState.getInt(SESSION_ID); mInstallId = savedInstanceState.getInt(INSTALL_ID); // Reregister for result; might instantly call back if result was delivered while // activity was destroyed try { InstallEventReceiver.addObserver(this, mInstallId, this::launchFinishBasedOnResult); } catch (EventResultPersister.OutOfIdsException e) { // Does not happen } } else { PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( PackageInstaller.SessionParams.MODE_FULL_INSTALL); Loading Loading @@ -132,6 +135,14 @@ public class InstallInstalling extends Activity { params.setSize(file.length()); } try { mInstallId = InstallEventReceiver .addObserver(this, EventResultPersister.GENERATE_NEW_ID, this::launchFinishBasedOnResult); } catch (EventResultPersister.OutOfIdsException e) { launchFailure(PackageInstaller.STATUS_FAILURE, null); } try { mSessionId = getPackageManager().getPackageInstaller().createSession(params); } catch (IOException e) { Loading @@ -155,12 +166,6 @@ public class InstallInstalling extends Activity { finish(); }); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(BROADCAST_ACTION); mBroadcastReceiver = new InstallResultReceiver(); registerReceiver(mBroadcastReceiver, intentFilter, BROADCAST_SENDER_PERMISSION, null); mSessionCallback = new InstallSessionCallback(); } } Loading @@ -183,11 +188,6 @@ public class InstallInstalling extends Activity { * @param statusCode The status code explaining what went wrong */ private void launchFailure(int statusCode, String statusMessage) { if (mSessionId > 0) { getPackageManager().getPackageInstaller().abandonSession(mSessionId); mSessionId = 0; } Intent failureIntent = new Intent(getIntent()); failureIntent.setClass(this, InstallFailed.class); failureIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); Loading Loading @@ -229,6 +229,7 @@ public class InstallInstalling extends Activity { super.onSaveInstanceState(outState); outState.putInt(SESSION_ID, mSessionId); outState.putInt(INSTALL_ID, mInstallId); } @Override Loading @@ -254,36 +255,22 @@ public class InstallInstalling extends Activity { } } if (mBroadcastReceiver != null) { unregisterReceiver(mBroadcastReceiver); } InstallEventReceiver.removeObserver(this, mInstallId); super.onDestroy(); } /** * Receive results from the package installer after InstallingAsyncTask finished. * Launch the appropriate finish activity (success or failed) for the installation result. * * @param statusCode The installation result. * @param statusMessage The detailed installation result. */ private final class InstallResultReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (intent.getIntExtra(SESSION_ID, 0) != mSessionId) { return; } final int statusCode = intent.getIntExtra( PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); if (statusCode == PackageInstaller.STATUS_PENDING_USER_ACTION) { context.startActivity(intent.getParcelableExtra(Intent.EXTRA_INTENT)); } else { private void launchFinishBasedOnResult(int statusCode, String statusMessage) { if (statusCode == PackageInstaller.STATUS_SUCCESS) { launchSuccess(); } else { mSessionId = 0; launchFailure(statusCode, intent.getStringExtra( PackageInstaller.EXTRA_STATUS_MESSAGE)); } } launchFailure(statusCode, statusMessage); } } Loading Loading @@ -320,8 +307,8 @@ public class InstallInstalling extends Activity { } /** * Send the package to the package installer and then register a broadcast pending intent that * will wake up {@link InstallResultReceiver} * Send the package to the package installer and then register a event result observer that * will call {@link #launchFinishBasedOnResult(int, String)} */ private final class InstallingAsyncTask extends AsyncTask<Void, Void, PackageInstaller.Session> { Loading Loading @@ -368,17 +355,18 @@ public class InstallInstalling extends Activity { } } synchronized (this) { isDone = true; notifyAll(); } return session; } catch (IOException e) { Log.e(LOG_TAG, "Could not write package", e); session.close(); return null; } finally { synchronized (this) { isDone = true; notifyAll(); } } } Loading @@ -386,17 +374,18 @@ public class InstallInstalling extends Activity { protected void onPostExecute(PackageInstaller.Session session) { if (session != null) { Intent broadcastIntent = new Intent(BROADCAST_ACTION); broadcastIntent.putExtra(SESSION_ID, mSessionId); broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, mInstallId); PendingIntent pendingIntent = PendingIntent.getBroadcast( InstallInstalling.this, mSessionId, mInstallId, broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT); session.commit(pendingIntent.getIntentSender()); mCancelButton.setEnabled(false); } else { getPackageManager().getPackageInstaller().abandonSession(mSessionId); launchFailure(PackageInstaller.STATUS_FAILURE, null); } } Loading Loading
AndroidManifest.xml +10 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.KILL_UID" /> <uses-permission android:name="android.permission.MANAGE_APP_OPS_RESTRICTIONS" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="com.google.android.permission.INSTALL_WEARABLE_PACKAGES" /> Loading Loading @@ -63,6 +64,15 @@ android:theme="@style/DialogWhenLargeNoAnimation" android:exported="false" /> <receiver android:name=".InstallEventReceiver" android:permission="android.permission.INSTALL_PACKAGES" android:exported="true"> <intent-filter> <action android:name="com.android.packageinstaller.ACTION_INSTALL_COMMIT" /> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver> <activity android:name=".InstallSuccess" android:theme="@style/DialogWhenLargeNoAnimation" android:exported="false" /> Loading
src/com/android/packageinstaller/EventResultPersister.java 0 → 100644 +315 −0 Original line number Diff line number Diff line /* * 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; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInstaller; import android.os.AsyncTask; import android.util.AtomicFile; import android.util.Log; import android.util.SparseArray; import android.util.Xml; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.FastXmlSerializer; import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; /** * Persists results of events and calls back observers when a matching result arrives. */ class EventResultPersister { private static final String LOG_TAG = EventResultPersister.class.getSimpleName(); /** Id passed to {@link #addObserver(int, EventResultObserver)} to generate new id */ static final int GENERATE_NEW_ID = Integer.MIN_VALUE; /** * The extra with the id to set in the intent delivered to * {@link #onEventReceived(Context, Intent)} */ static final String EXTRA_ID = "EventResultPersister.EXTRA_ID"; /** Persisted state of this object */ private final AtomicFile mResultsFile; private final Object mLock = new Object(); /** Currently stored but not yet called back results (install id -> status, status message) */ @GuardedBy("mLock") private final SparseArray<EventResult> mResults = new SparseArray<>(); /** Currently registered, not called back observers (install id -> observer) */ @GuardedBy("mLock") private final SparseArray<EventResultObserver> mObservers = new SparseArray<>(); /** Always increasing counter for install event ids */ @GuardedBy("mLock") private int mCounter; /** If a write that will persist the state is scheduled */ @GuardedBy("mLock") private boolean mIsPersistScheduled; /** If the state was changed while the data was being persisted */ @GuardedBy("mLock") private boolean mIsPersistingStateValid; /** Call back when a result is received. Observer is removed when onResult it called. */ interface EventResultObserver { void onResult(int status, @Nullable String message); } /** * Read persisted state. * * @param resultFile The file the results are persisted in */ EventResultPersister(@NonNull File resultFile) { mResultsFile = new AtomicFile(resultFile); mCounter = GENERATE_NEW_ID + 1; try (FileInputStream stream = mResultsFile.openRead()) { XmlPullParser parser = Xml.newPullParser(); parser.setInput(stream, StandardCharsets.UTF_8.name()); XmlUtils.nextElement(parser); while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { String tagName = parser.getName(); if ("results".equals(tagName)) { mCounter = XmlUtils.readIntAttribute(parser, "counter", GENERATE_NEW_ID + 1); } else if ("result".equals(tagName)) { int id = XmlUtils.readIntAttribute(parser, "id", 0); int status = XmlUtils.readIntAttribute(parser, "status", 0); String statusMessage = XmlUtils.readStringAttribute(parser, "statusMessage"); if (mResults.get(id) != null) { throw new Exception("id " + id + " has two results"); } mResults.put(id, new EventResult(status, statusMessage)); } else { throw new Exception("unexpected tag"); } XmlUtils.nextElement(parser); } } catch (Exception e) { mResults.clear(); writeState(); } } /** * Add a result. If the result is an pending user action, execute the pending user action * directly and do not queue a result. * * @param context The context the event was received in * @param intent The intent the activity received */ void onEventReceived(@NonNull Context context, @NonNull Intent intent) { int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0); if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) { context.startActivity(intent.getParcelableExtra(Intent.EXTRA_INTENT)); return; } int id = intent.getIntExtra(EXTRA_ID, 0); String statusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE); EventResultObserver observerToCall = null; synchronized (mLock) { int numObservers = mObservers.size(); for (int i = 0; i < numObservers; i++) { if (mObservers.keyAt(i) == id) { observerToCall = mObservers.valueAt(i); mObservers.removeAt(i); break; } } if (observerToCall != null) { observerToCall.onResult(status, statusMessage); } else { mResults.put(id, new EventResult(status, statusMessage)); writeState(); } } } /** * Persist current state. The persistence might be delayed. */ private void writeState() { synchronized (mLock) { mIsPersistingStateValid = false; if (!mIsPersistScheduled) { mIsPersistScheduled = true; AsyncTask.execute(() -> { int counter; SparseArray<EventResult> results; while (true) { // Take snapshot of state synchronized (mLock) { counter = mCounter; results = mResults.clone(); mIsPersistingStateValid = true; } FileOutputStream stream = null; try { stream = mResultsFile.startWrite(); FastXmlSerializer serializer = new FastXmlSerializer(); serializer.setOutput(stream, StandardCharsets.UTF_8.name()); serializer.startDocument(null, true); serializer.setFeature( "http://xmlpull.org/v1/doc/features.html#indent-output", true); serializer.startTag(null, "results"); serializer.attribute(null, "counter", Integer.toString(counter)); int numResults = results.size(); for (int i = 0; i < numResults; i++) { serializer.startTag(null, "result"); serializer.attribute(null, "id", Integer.toString(results.keyAt(i))); serializer.attribute(null, "status", Integer.toString(results.valueAt(i).status)); if (results.valueAt(i).message != null) { serializer.attribute(null, "statusMessage", results.valueAt(i).message); } serializer.endTag(null, "result"); } serializer.endTag(null, "results"); serializer.endDocument(); mResultsFile.finishWrite(stream); } catch (IOException e) { if (stream != null) { mResultsFile.failWrite(stream); } Log.e(LOG_TAG, "error writing results", e); mResultsFile.delete(); } // Check if there was changed state since we persisted. If so, we need to // persist again. synchronized (mLock) { if (mIsPersistingStateValid) { mIsPersistScheduled = false; break; } } } }); } } } /** * Add an observer. If there is already an event for this id, call back inside of this call. * * @param id The id the observer is for or {@code GENERATE_NEW_ID} to generate a new one. * @param observer The observer to call back. * * @return The id for this event */ int addObserver(int id, @NonNull EventResultObserver observer) throws OutOfIdsException { boolean stateChanged = false; synchronized (mLock) { int resultIndex = 0; EventResult result = null; if (id == GENERATE_NEW_ID) { if (mCounter == Integer.MAX_VALUE) { throw new OutOfIdsException(); } else { id = mCounter; mCounter++; stateChanged = true; } } else { resultIndex = mResults.indexOfKey(id); result = mResults.valueAt(resultIndex); } // Check if we can instantly call back if (result != null) { observer.onResult(result.status, result.message); mResults.removeAt(resultIndex); stateChanged = true; } else { mObservers.put(id, observer); } if (stateChanged) { writeState(); } } return id; } /** * Remove a observer. * * @param id The id the observer was added for */ void removeObserver(int id) { synchronized (mLock) { mObservers.delete(id); } } /** * The status from an event. */ private class EventResult { public final int status; @Nullable public final String message; private EventResult(int status, @Nullable String message) { this.status = status; this.message = message; } } class OutOfIdsException extends Exception {} }
src/com/android/packageinstaller/InstallEventReceiver.java 0 → 100644 +85 −0 Original line number Diff line number Diff line /* * 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; import android.annotation.NonNull; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import java.io.File; /** * Receives install events and perists them using a {@link EventResultPersister}. */ public class InstallEventReceiver extends BroadcastReceiver { private static final Object sLock = new Object(); private static EventResultPersister sReceiver; /** * Get the event receiver persisting the results * * @return The event receiver. */ @NonNull private static EventResultPersister getReceiver(@NonNull Context context) { synchronized (sLock) { if (sReceiver == null) { sReceiver = new EventResultPersister(new File(context.getNoBackupFilesDir(), "install_results.xml")); } } return sReceiver; } @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) { // Do not persist installs over reboot synchronized (sLock) { new File(context.getNoBackupFilesDir(), "install_results.xml").delete(); } } else { getReceiver(context).onEventReceived(context, intent); } } /** * Add an observer. If there is already an event for this id, call back inside of this call. * * @param context A context of the current app * @param id The id the observer is for or {@code GENERATE_NEW_ID} to generate a new one. * @param observer The observer to call back. * * @return The id for this event */ static int addObserver(@NonNull Context context, int id, @NonNull EventResultPersister.EventResultObserver observer) throws EventResultPersister.OutOfIdsException { return getReceiver(context).addObserver(id, observer); } /** * Remove a observer. * * @param context A context of the current app * @param id The id the observer was added for */ static void removeObserver(@NonNull Context context, int id) { getReceiver(context).removeObserver(id); } }
src/com/android/packageinstaller/InstallInstalling.java +48 −59 Original line number Diff line number Diff line Loading @@ -21,10 +21,7 @@ import static android.content.pm.PackageInstaller.SessionParams.UID_UNKNOWN; import android.annotation.Nullable; import android.app.Activity; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; Loading Loading @@ -54,16 +51,11 @@ public class InstallInstalling extends Activity { private static final String LOG_TAG = InstallInstalling.class.getSimpleName(); private static final String SESSION_ID = "com.android.packageinstaller.SESSION_ID"; private static final String INSTALL_ID = "com.android.packageinstaller.INSTALL_ID"; private static final String BROADCAST_ACTION = "com.android.packageinstaller.ACTION_INSTALL_COMMIT"; private static final String BROADCAST_SENDER_PERMISSION = "android.permission.INSTALL_PACKAGES"; /** Receiver receiving the results of the installation */ private BroadcastReceiver mBroadcastReceiver; /** Listens to changed to the session and updates progress bar */ private PackageInstaller.SessionCallback mSessionCallback; Loading @@ -73,12 +65,12 @@ public class InstallInstalling extends Activity { /** Id of the session to install the package */ private int mSessionId; /** Id of the install event we wait for */ private int mInstallId; /** URI of package to install */ private Uri mPackageURI; /** Info about the app to info */ private ApplicationInfo mAppInfo; /** The button that can cancel this dialog */ private Button mCancelButton; Loading @@ -88,23 +80,34 @@ public class InstallInstalling extends Activity { setContentView(R.layout.install_installing); mAppInfo = getIntent().getParcelableExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO); ApplicationInfo appInfo = getIntent() .getParcelableExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO); mPackageURI = getIntent().getData(); if ("package".equals(mPackageURI.getScheme())) { try { getPackageManager().installExistingPackage(mAppInfo.packageName); getPackageManager().installExistingPackage(appInfo.packageName); launchSuccess(); } catch (PackageManager.NameNotFoundException e) { launchFailure(PackageInstaller.STATUS_FAILURE_INVALID, null); } } else { final File sourceFile = new File(mPackageURI.getPath()); PackageUtil.initSnippetForNewApp(this, PackageUtil.getAppSnippet(this, mAppInfo, PackageUtil.initSnippetForNewApp(this, PackageUtil.getAppSnippet(this, appInfo, sourceFile), R.id.app_snippet); if (savedInstanceState != null) { mSessionId = savedInstanceState.getInt(SESSION_ID); mInstallId = savedInstanceState.getInt(INSTALL_ID); // Reregister for result; might instantly call back if result was delivered while // activity was destroyed try { InstallEventReceiver.addObserver(this, mInstallId, this::launchFinishBasedOnResult); } catch (EventResultPersister.OutOfIdsException e) { // Does not happen } } else { PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( PackageInstaller.SessionParams.MODE_FULL_INSTALL); Loading Loading @@ -132,6 +135,14 @@ public class InstallInstalling extends Activity { params.setSize(file.length()); } try { mInstallId = InstallEventReceiver .addObserver(this, EventResultPersister.GENERATE_NEW_ID, this::launchFinishBasedOnResult); } catch (EventResultPersister.OutOfIdsException e) { launchFailure(PackageInstaller.STATUS_FAILURE, null); } try { mSessionId = getPackageManager().getPackageInstaller().createSession(params); } catch (IOException e) { Loading @@ -155,12 +166,6 @@ public class InstallInstalling extends Activity { finish(); }); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(BROADCAST_ACTION); mBroadcastReceiver = new InstallResultReceiver(); registerReceiver(mBroadcastReceiver, intentFilter, BROADCAST_SENDER_PERMISSION, null); mSessionCallback = new InstallSessionCallback(); } } Loading @@ -183,11 +188,6 @@ public class InstallInstalling extends Activity { * @param statusCode The status code explaining what went wrong */ private void launchFailure(int statusCode, String statusMessage) { if (mSessionId > 0) { getPackageManager().getPackageInstaller().abandonSession(mSessionId); mSessionId = 0; } Intent failureIntent = new Intent(getIntent()); failureIntent.setClass(this, InstallFailed.class); failureIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); Loading Loading @@ -229,6 +229,7 @@ public class InstallInstalling extends Activity { super.onSaveInstanceState(outState); outState.putInt(SESSION_ID, mSessionId); outState.putInt(INSTALL_ID, mInstallId); } @Override Loading @@ -254,36 +255,22 @@ public class InstallInstalling extends Activity { } } if (mBroadcastReceiver != null) { unregisterReceiver(mBroadcastReceiver); } InstallEventReceiver.removeObserver(this, mInstallId); super.onDestroy(); } /** * Receive results from the package installer after InstallingAsyncTask finished. * Launch the appropriate finish activity (success or failed) for the installation result. * * @param statusCode The installation result. * @param statusMessage The detailed installation result. */ private final class InstallResultReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (intent.getIntExtra(SESSION_ID, 0) != mSessionId) { return; } final int statusCode = intent.getIntExtra( PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); if (statusCode == PackageInstaller.STATUS_PENDING_USER_ACTION) { context.startActivity(intent.getParcelableExtra(Intent.EXTRA_INTENT)); } else { private void launchFinishBasedOnResult(int statusCode, String statusMessage) { if (statusCode == PackageInstaller.STATUS_SUCCESS) { launchSuccess(); } else { mSessionId = 0; launchFailure(statusCode, intent.getStringExtra( PackageInstaller.EXTRA_STATUS_MESSAGE)); } } launchFailure(statusCode, statusMessage); } } Loading Loading @@ -320,8 +307,8 @@ public class InstallInstalling extends Activity { } /** * Send the package to the package installer and then register a broadcast pending intent that * will wake up {@link InstallResultReceiver} * Send the package to the package installer and then register a event result observer that * will call {@link #launchFinishBasedOnResult(int, String)} */ private final class InstallingAsyncTask extends AsyncTask<Void, Void, PackageInstaller.Session> { Loading Loading @@ -368,17 +355,18 @@ public class InstallInstalling extends Activity { } } synchronized (this) { isDone = true; notifyAll(); } return session; } catch (IOException e) { Log.e(LOG_TAG, "Could not write package", e); session.close(); return null; } finally { synchronized (this) { isDone = true; notifyAll(); } } } Loading @@ -386,17 +374,18 @@ public class InstallInstalling extends Activity { protected void onPostExecute(PackageInstaller.Session session) { if (session != null) { Intent broadcastIntent = new Intent(BROADCAST_ACTION); broadcastIntent.putExtra(SESSION_ID, mSessionId); broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, mInstallId); PendingIntent pendingIntent = PendingIntent.getBroadcast( InstallInstalling.this, mSessionId, mInstallId, broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT); session.commit(pendingIntent.getIntentSender()); mCancelButton.setEnabled(false); } else { getPackageManager().getPackageInstaller().abandonSession(mSessionId); launchFailure(PackageInstaller.STATUS_FAILURE, null); } } Loading