1 /*
2  * Copyright (C) 2009 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.ide.eclipse.adt.internal.project;
18 
19 import com.android.ddmlib.AndroidDebugBridge;
20 import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener;
21 import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
22 import com.android.ddmlib.IDevice;
23 import com.android.ddmlib.MultiLineReceiver;
24 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor;
25 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener;
26 
27 import org.eclipse.core.resources.IProject;
28 import org.eclipse.core.runtime.IPath;
29 
30 import java.util.HashSet;
31 import java.util.Iterator;
32 
33 /**
34  * Registers which apk was installed on which device.
35  * <p/>
36  * The goal of this class is to remember the installation of APKs on devices, and provide
37  * information about whether a new APK should be installed on a device prior to running the
38  * application from a launch configuration.
39  * <p/>
40  * The manager uses {@link IProject} and {@link IDevice} to identify the target device and the
41  * (project generating the) APK. This ensures that disconnected and reconnected devices will
42  * always receive new APKs (since the version may not match).
43  * <p/>
44  * This is a singleton. To get the instance, use {@link #getInstance()}
45  */
46 public final class ApkInstallManager {
47 
48     private final static ApkInstallManager sThis = new ApkInstallManager();
49 
50     /**
51      * Internal struct to associate a project and a device.
52      */
53     private final static class ApkInstall {
ApkInstall(IProject project, String packageName, IDevice device)54         public ApkInstall(IProject project, String packageName, IDevice device) {
55             this.project = project;
56             this.packageName = packageName;
57             this.device = device;
58         }
59 
60         @Override
equals(Object obj)61         public boolean equals(Object obj) {
62             if (obj instanceof ApkInstall) {
63                 ApkInstall apkObj = (ApkInstall)obj;
64 
65                 return (device == apkObj.device && project.equals(apkObj.project) &&
66                         packageName.equals(apkObj.packageName));
67             }
68 
69             return false;
70         }
71 
72         @Override
hashCode()73         public int hashCode() {
74             return (device.getSerialNumber() + project.getName() + packageName).hashCode();
75         }
76 
77         final IProject project;
78         final String packageName;
79         final IDevice device;
80     }
81 
82     /**
83      * Receiver and parser for the "pm path package" command.
84      */
85     private final static class PmReceiver extends MultiLineReceiver {
86         boolean foundPackage = false;
87         @Override
processNewLines(String[] lines)88         public void processNewLines(String[] lines) {
89             // if the package if found, then pm will show a line starting with "package:/"
90             if (foundPackage == false) { // just in case this is called several times for multilines
91                 for (String line : lines) {
92                     if (line.startsWith("package:/")) {
93                         foundPackage = true;
94                         break;
95                     }
96                 }
97             }
98         }
99 
100         @Override
isCancelled()101         public boolean isCancelled() {
102             return false;
103         }
104     }
105 
106     /**
107      * Hashset of the list of installed package. Hashset used to ensure we don't re-add new
108      * objects for the same app.
109      */
110     private final HashSet<ApkInstall> mInstallList = new HashSet<ApkInstall>();
111 
getInstance()112     public static ApkInstallManager getInstance() {
113         return sThis;
114     }
115 
116     /**
117      * Registers an installation of <var>project</var> onto <var>device</var>
118      * @param project The project that was installed.
119      * @param packageName the package name of the apk
120      * @param device The device that received the installation.
121      */
registerInstallation(IProject project, String packageName, IDevice device)122     public void registerInstallation(IProject project, String packageName, IDevice device) {
123         synchronized (mInstallList) {
124             mInstallList.add(new ApkInstall(project, packageName, device));
125         }
126     }
127 
128     /**
129      * Returns whether a <var>project</var> was installed on the <var>device</var>.
130      * @param project the project that may have been installed.
131      * @param device the device that may have received the installation.
132      * @return
133      */
isApplicationInstalled(IProject project, String packageName, IDevice device)134     public boolean isApplicationInstalled(IProject project, String packageName, IDevice device) {
135         synchronized (mInstallList) {
136             ApkInstall found = null;
137             for (ApkInstall install : mInstallList) {
138                 if (project.equals(install.project) && packageName.equals(install.packageName) &&
139                         device == install.device) {
140                     found = install;
141                     break;
142                 }
143             }
144 
145             // check the app is still installed.
146             if (found != null) {
147                 try {
148                     PmReceiver receiver = new PmReceiver();
149                     found.device.executeShellCommand("pm path " + packageName, receiver);
150                     if (receiver.foundPackage == false) {
151                         mInstallList.remove(found);
152                     }
153 
154                     return receiver.foundPackage;
155                 } catch (Exception e) {
156                     // failed to query pm? force reinstall.
157                     return false;
158                 }
159             }
160         }
161         return false;
162     }
163 
164     /**
165      * Resets registered installations for a specific {@link IProject}.
166      * <p/>This ensures that {@link #isApplicationInstalled(IProject, IDevice)} will always return
167      * <code>null</code> for this specified project, for any device.
168      * @param project the project for which to reset all installations.
169      */
resetInstallationFor(IProject project)170     public void resetInstallationFor(IProject project) {
171         synchronized (mInstallList) {
172             Iterator<ApkInstall> iterator = mInstallList.iterator();
173             while (iterator.hasNext()) {
174                 ApkInstall install = iterator.next();
175                 if (install.project.equals(project)) {
176                     iterator.remove();
177                 }
178             }
179         }
180     }
181 
ApkInstallManager()182     private ApkInstallManager() {
183         AndroidDebugBridge.addDeviceChangeListener(mDeviceChangeListener);
184         AndroidDebugBridge.addDebugBridgeChangeListener(mDebugBridgeListener);
185         GlobalProjectMonitor.getMonitor().addProjectListener(mProjectListener);
186     }
187 
188     private IDebugBridgeChangeListener mDebugBridgeListener = new IDebugBridgeChangeListener() {
189         /**
190          * Responds to a bridge change by clearing the full installation list.
191          *
192          * @see IDebugBridgeChangeListener#bridgeChanged(AndroidDebugBridge)
193          */
194         @Override
195         public void bridgeChanged(AndroidDebugBridge bridge) {
196             // the bridge changed, there is no way to know which IDevice will be which.
197             // We reset everything
198             synchronized (mInstallList) {
199                 mInstallList.clear();
200             }
201         }
202     };
203 
204     private IDeviceChangeListener mDeviceChangeListener = new IDeviceChangeListener() {
205         /**
206          * Responds to a device being disconnected by removing all installations related
207          * to this device.
208          *
209          * @see IDeviceChangeListener#deviceDisconnected(IDevice)
210          */
211         @Override
212         public void deviceDisconnected(IDevice device) {
213             synchronized (mInstallList) {
214                 Iterator<ApkInstall> iterator = mInstallList.iterator();
215                 while (iterator.hasNext()) {
216                     ApkInstall install = iterator.next();
217                     if (install.device == device) {
218                         iterator.remove();
219                     }
220                 }
221             }
222         }
223 
224         @Override
225         public void deviceChanged(IDevice device, int changeMask) {
226             // nothing to do.
227         }
228 
229         @Override
230         public void deviceConnected(IDevice device) {
231             // nothing to do.
232         }
233     };
234 
235     private IProjectListener mProjectListener = new IProjectListener() {
236         /**
237          * Responds to a closed project by resetting all its installation.
238          *
239          * @see IProjectListener#projectClosed(IProject)
240          */
241         @Override
242         public void projectClosed(IProject project) {
243             resetInstallationFor(project);
244         }
245 
246         /**
247          * Responds to a deleted project by resetting all its installation.
248          *
249          * @see IProjectListener#projectDeleted(IProject)
250          */
251         @Override
252         public void projectDeleted(IProject project) {
253             resetInstallationFor(project);
254         }
255 
256         @Override
257         public void projectOpened(IProject project) {
258             // nothing to do.
259         }
260 
261         @Override
262         public void projectOpenedWithWorkspace(IProject project) {
263             // nothing to do.
264         }
265 
266         @Override
267         public void allProjectsOpenedWithWorkspace() {
268             // nothing to do.
269         }
270 
271         @Override
272         public void projectRenamed(IProject project, IPath from) {
273             // project renaming also triggers delete/open events so
274             // there's nothing to do here (since delete will remove
275             // whatever's linked to the project from the list).
276         }
277     };
278 }
279