1/*
2**
3** Copyright 2007, The Android Open Source Project
4**
5** Licensed under the Apache License, Version 2.0 (the "License");
6** you may not use this file except in compliance with the License.
7** You may obtain a copy of the License at
8**
9**     http://www.apache.org/licenses/LICENSE-2.0
10**
11** Unless required by applicable law or agreed to in writing, software
12** distributed under the License is distributed on an "AS IS" BASIS,
13** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14** See the License for the specific language governing permissions and
15** limitations under the License.
16*/
17package com.android.packageinstaller;
18
19import android.app.Activity;
20import android.app.AlertDialog;
21import android.app.Dialog;
22import android.content.Context;
23import android.content.DialogInterface;
24import android.content.DialogInterface.OnCancelListener;
25import android.content.Intent;
26import android.content.SharedPreferences;
27import android.content.pm.ApplicationInfo;
28import android.content.pm.PackageManager;
29import android.content.pm.PackageManager.NameNotFoundException;
30import android.content.pm.PackageParser;
31import android.graphics.Rect;
32import android.net.Uri;
33import android.os.Bundle;
34import android.provider.Settings;
35import android.util.Log;
36import android.view.LayoutInflater;
37import android.view.View;
38import android.view.View.OnClickListener;
39import android.view.ViewGroup;
40import android.widget.AppSecurityPermissions;
41import android.widget.Button;
42import android.widget.LinearLayout;
43import android.widget.ScrollView;
44import android.widget.TabHost;
45import android.widget.TabWidget;
46import android.widget.TextView;
47
48import java.io.File;
49import java.util.ArrayList;
50
51/*
52 * This activity is launched when a new application is installed via side loading
53 * The package is first parsed and the user is notified of parse errors via a dialog.
54 * If the package is successfully parsed, the user is notified to turn on the install unknown
55 * applications setting. A memory check is made at this point and the user is notified of out
56 * of memory conditions if any. If the package is already existing on the device,
57 * a confirmation dialog (to replace the existing package) is presented to the user.
58 * Based on the user response the package is then installed by launching InstallAppConfirm
59 * sub activity. All state transitions are handled in this activity
60 */
61public class PackageInstallerActivity extends Activity implements OnCancelListener, OnClickListener {
62    private static final String TAG = "PackageInstaller";
63    private Uri mPackageURI;
64    private boolean localLOGV = false;
65    PackageManager mPm;
66    PackageParser.Package mPkgInfo;
67    ApplicationInfo mSourceInfo;
68
69    // ApplicationInfo object primarily used for already existing applications
70    private ApplicationInfo mAppInfo = null;
71
72    // View for install progress
73    View mInstallConfirm;
74    // Buttons to indicate user acceptance
75    private Button mOk;
76    private Button mCancel;
77
78    static final String PREFS_ALLOWED_SOURCES = "allowed_sources";
79
80    // Dialog identifiers used in showDialog
81    private static final int DLG_BASE = 0;
82    private static final int DLG_REPLACE_APP = DLG_BASE + 1;
83    private static final int DLG_UNKNOWN_APPS = DLG_BASE + 2;
84    private static final int DLG_PACKAGE_ERROR = DLG_BASE + 3;
85    private static final int DLG_OUT_OF_SPACE = DLG_BASE + 4;
86    private static final int DLG_INSTALL_ERROR = DLG_BASE + 5;
87    private static final int DLG_ALLOW_SOURCE = DLG_BASE + 6;
88
89    private void startInstallConfirm() {
90        LinearLayout permsSection = (LinearLayout) mInstallConfirm.findViewById(R.id.permissions_section);
91        LinearLayout securityList = (LinearLayout) permsSection.findViewById(
92                R.id.security_settings_list);
93        boolean permVisible = false;
94        if(mPkgInfo != null) {
95            AppSecurityPermissions asp = new AppSecurityPermissions(this, mPkgInfo);
96            if(asp.getPermissionCount() > 0) {
97                permVisible = true;
98                securityList.addView(asp.getPermissionsView());
99            }
100        }
101        if(!permVisible){
102            permsSection.setVisibility(View.INVISIBLE);
103        }
104        mInstallConfirm.setVisibility(View.VISIBLE);
105        mOk = (Button)findViewById(R.id.ok_button);
106        mCancel = (Button)findViewById(R.id.cancel_button);
107        mOk.setOnClickListener(this);
108        mCancel.setOnClickListener(this);
109    }
110
111    private void showDialogInner(int id) {
112        // TODO better fix for this? Remove dialog so that it gets created again
113        removeDialog(id);
114        showDialog(id);
115    }
116
117    @Override
118    public Dialog onCreateDialog(int id, Bundle bundle) {
119        switch (id) {
120        case DLG_REPLACE_APP:
121            int msgId = R.string.dlg_app_replacement_statement;
122            // Customized text for system apps
123            if ((mAppInfo != null) && (mAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
124                msgId = R.string.dlg_sys_app_replacement_statement;
125            }
126            return new AlertDialog.Builder(this)
127                    .setTitle(R.string.dlg_app_replacement_title)
128                    .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
129                        public void onClick(DialogInterface dialog, int which) {
130                            startInstallConfirm();
131                        }})
132                    .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
133                        public void onClick(DialogInterface dialog, int which) {
134                            Log.i(TAG, "Canceling installation");
135                            setResult(RESULT_CANCELED);
136                            finish();
137                        }})
138                    .setMessage(msgId)
139                    .setOnCancelListener(this)
140                    .create();
141        case DLG_UNKNOWN_APPS:
142            return new AlertDialog.Builder(this)
143                    .setTitle(R.string.unknown_apps_dlg_title)
144                    .setMessage(R.string.unknown_apps_dlg_text)
145                    .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
146                        public void onClick(DialogInterface dialog, int which) {
147                            Log.i(TAG, "Finishing off activity so that user can navigate to settings manually");
148                            finish();
149                        }})
150                    .setPositiveButton(R.string.settings, new DialogInterface.OnClickListener() {
151                        public void onClick(DialogInterface dialog, int which) {
152                            Log.i(TAG, "Launching settings");
153                            launchSettingsAppAndFinish();
154                        }
155                    })
156                    .setOnCancelListener(this)
157                    .create();
158        case DLG_PACKAGE_ERROR :
159            return new AlertDialog.Builder(this)
160                    .setTitle(R.string.Parse_error_dlg_title)
161                    .setMessage(R.string.Parse_error_dlg_text)
162                    .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
163                        public void onClick(DialogInterface dialog, int which) {
164                            finish();
165                        }
166                    })
167                    .setOnCancelListener(this)
168                    .create();
169        case DLG_OUT_OF_SPACE:
170            // Guaranteed not to be null. will default to package name if not set by app
171            CharSequence appTitle = mPm.getApplicationLabel(mPkgInfo.applicationInfo);
172            String dlgText = getString(R.string.out_of_space_dlg_text,
173                    appTitle.toString());
174            return new AlertDialog.Builder(this)
175                    .setTitle(R.string.out_of_space_dlg_title)
176                    .setMessage(dlgText)
177                    .setPositiveButton(R.string.manage_applications, new DialogInterface.OnClickListener() {
178                        public void onClick(DialogInterface dialog, int which) {
179                            //launch manage applications
180                            Intent intent = new Intent("android.intent.action.MANAGE_PACKAGE_STORAGE");
181                            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
182                            startActivity(intent);
183                            finish();
184                        }
185                    })
186                    .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
187                        public void onClick(DialogInterface dialog, int which) {
188                            Log.i(TAG, "Canceling installation");
189                            finish();
190                        }
191                  })
192                  .setOnCancelListener(this)
193                  .create();
194        case DLG_INSTALL_ERROR :
195            // Guaranteed not to be null. will default to package name if not set by app
196            CharSequence appTitle1 = mPm.getApplicationLabel(mPkgInfo.applicationInfo);
197            String dlgText1 = getString(R.string.install_failed_msg,
198                    appTitle1.toString());
199            return new AlertDialog.Builder(this)
200                    .setTitle(R.string.install_failed)
201                    .setNeutralButton(R.string.ok, new DialogInterface.OnClickListener() {
202                        public void onClick(DialogInterface dialog, int which) {
203                            finish();
204                        }
205                    })
206                    .setMessage(dlgText1)
207                    .setOnCancelListener(this)
208                    .create();
209        case DLG_ALLOW_SOURCE:
210            CharSequence appTitle2 = mPm.getApplicationLabel(mSourceInfo);
211            String dlgText2 = getString(R.string.allow_source_dlg_text,
212                    appTitle2.toString());
213            return new AlertDialog.Builder(this)
214                    .setTitle(R.string.allow_source_dlg_title)
215                    .setMessage(dlgText2)
216                    .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
217                        public void onClick(DialogInterface dialog, int which) {
218                            setResult(RESULT_CANCELED);
219                            finish();
220                        }})
221                    .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
222                        public void onClick(DialogInterface dialog, int which) {
223                            SharedPreferences prefs = getSharedPreferences(PREFS_ALLOWED_SOURCES,
224                                    Context.MODE_PRIVATE);
225                            prefs.edit().putBoolean(mSourceInfo.packageName, true).apply();
226                            startInstallConfirm();
227                        }
228                    })
229                    .setOnCancelListener(this)
230                    .create();
231       }
232       return null;
233   }
234
235    private void launchSettingsAppAndFinish() {
236        // Create an intent to launch SettingsTwo activity
237        Intent launchSettingsIntent = new Intent(Settings.ACTION_SECURITY_SETTINGS);
238        launchSettingsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
239        startActivity(launchSettingsIntent);
240        finish();
241    }
242
243    private boolean isInstallingUnknownAppsAllowed() {
244        return Settings.Secure.getInt(getContentResolver(),
245            Settings.Secure.INSTALL_NON_MARKET_APPS, 0) > 0;
246    }
247
248    private void initiateInstall() {
249        String pkgName = mPkgInfo.packageName;
250        // Check if there is already a package on the device with this name
251        // but it has been renamed to something else.
252        String[] oldName = mPm.canonicalToCurrentPackageNames(new String[] { pkgName });
253        if (oldName != null && oldName.length > 0 && oldName[0] != null) {
254            pkgName = oldName[0];
255            mPkgInfo.setPackageName(pkgName);
256        }
257        // Check if package is already installed. display confirmation dialog if replacing pkg
258        try {
259            mAppInfo = mPm.getApplicationInfo(pkgName,
260                    PackageManager.GET_UNINSTALLED_PACKAGES);
261        } catch (NameNotFoundException e) {
262            mAppInfo = null;
263        }
264        if (mAppInfo == null || getIntent().getBooleanExtra(Intent.EXTRA_ALLOW_REPLACE, false)) {
265            startInstallConfirm();
266        } else {
267            if(localLOGV) Log.i(TAG, "Replacing existing package:"+
268                    mPkgInfo.applicationInfo.packageName);
269            showDialogInner(DLG_REPLACE_APP);
270        }
271    }
272
273    void setPmResult(int pmResult) {
274        Intent result = new Intent();
275        result.putExtra(Intent.EXTRA_INSTALL_RESULT, pmResult);
276        setResult(pmResult == PackageManager.INSTALL_SUCCEEDED
277                ? RESULT_OK : RESULT_FIRST_USER, result);
278    }
279
280    @Override
281    protected void onCreate(Bundle icicle) {
282        super.onCreate(icicle);
283
284        // get intent information
285        final Intent intent = getIntent();
286        mPackageURI = intent.getData();
287        mPm = getPackageManager();
288
289        final String scheme = mPackageURI.getScheme();
290        if (scheme != null && !"file".equals(scheme)) {
291            throw new IllegalArgumentException("unexpected scheme " + scheme);
292        }
293
294        final File sourceFile = new File(mPackageURI.getPath());
295        mPkgInfo = PackageUtil.getPackageInfo(sourceFile);
296
297        // Check for parse errors
298        if (mPkgInfo == null) {
299            Log.w(TAG, "Parse error when parsing manifest. Discontinuing installation");
300            showDialogInner(DLG_PACKAGE_ERROR);
301            setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK);
302            return;
303        }
304
305        //set view
306        setContentView(R.layout.install_start);
307        mInstallConfirm = findViewById(R.id.install_confirm_panel);
308        mInstallConfirm.setVisibility(View.INVISIBLE);
309        final PackageUtil.AppSnippet as = PackageUtil.getAppSnippet(
310                this, mPkgInfo.applicationInfo, sourceFile);
311        PackageUtil.initSnippetForNewApp(this, as, R.id.app_snippet);
312
313        // Deal with install source.
314        String callerPackage = getCallingPackage();
315        if (callerPackage != null && intent.getBooleanExtra(
316                Intent.EXTRA_NOT_UNKNOWN_SOURCE, false)) {
317            try {
318                mSourceInfo = mPm.getApplicationInfo(callerPackage, 0);
319                if (mSourceInfo != null) {
320                    if ((mSourceInfo.flags&ApplicationInfo.FLAG_SYSTEM) != 0) {
321                        // System apps don't need to be approved.
322                        initiateInstall();
323                        return;
324                    }
325                    /* for now this is disabled, since the user would need to
326                     * have enabled the global "unknown sources" setting in the
327                     * first place in order to get here.
328                    SharedPreferences prefs = getSharedPreferences(PREFS_ALLOWED_SOURCES,
329                            Context.MODE_PRIVATE);
330                    if (prefs.getBoolean(mSourceInfo.packageName, false)) {
331                        // User has already allowed this one.
332                        initiateInstall();
333                        return;
334                    }
335                    //ask user to enable setting first
336                    showDialogInner(DLG_ALLOW_SOURCE);
337                    return;
338                     */
339                }
340            } catch (NameNotFoundException e) {
341            }
342        }
343
344        // Check unknown sources.
345        if (!isInstallingUnknownAppsAllowed()) {
346            //ask user to enable setting first
347            showDialogInner(DLG_UNKNOWN_APPS);
348            return;
349        }
350        initiateInstall();
351    }
352
353    // Generic handling when pressing back key
354    public void onCancel(DialogInterface dialog) {
355        finish();
356    }
357
358    public void onClick(View v) {
359        if(v == mOk) {
360            // Start subactivity to actually install the application
361            Intent newIntent = new Intent();
362            newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO,
363                    mPkgInfo.applicationInfo);
364            newIntent.setData(mPackageURI);
365            newIntent.setClass(this, InstallAppProgress.class);
366            String installerPackageName = getIntent().getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME);
367            if (installerPackageName != null) {
368                newIntent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, installerPackageName);
369            }
370            if (getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) {
371                newIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
372                newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
373            }
374            if(localLOGV) Log.i(TAG, "downloaded app uri="+mPackageURI);
375            startActivity(newIntent);
376            finish();
377        } else if(v == mCancel) {
378            // Cancel and finish
379            setResult(RESULT_CANCELED);
380            finish();
381        }
382    }
383
384    /**
385     * It's a ScrollView that knows how to stay awake.
386     */
387    static class CaffeinatedScrollView extends ScrollView {
388        public CaffeinatedScrollView(Context context) {
389            super(context);
390        }
391
392        /**
393         * Make this visible so we can call it
394         */
395        @Override
396        public boolean awakenScrollBars() {
397            return super.awakenScrollBars();
398        }
399    }
400}
401