Statistics
| Branch: | Tag: | Revision:

fdroidclient / F-Droid / src / org / fdroid / fdroid / AppDetails.java @ 69ecaf02

History | View | Annotate | Download (67.6 KB)

1
/*
2
 * Copyright (C) 2010-12  Ciaran Gultnieks, ciaran@ciarang.com
3
 * Copyright (C) 2013-15 Daniel Martí <mvdan@mvdan.cc>
4
 * Copyright (C) 2013 Stefan Völkel, bd@bc-bd.org
5
 * Copyright (C) 2015 Nico Alt, nicoalt@posteo.org
6
 *
7
 * This program is free software; you can redistribute it and/or
8
 * modify it under the terms of the GNU General Public License
9
 * as published by the Free Software Foundation; either version 3
10
 * of the License, or (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License
18
 * along with this program; if not, write to the Free Software
19
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
20
 */
21

    
22
package org.fdroid.fdroid;
23

    
24
import android.app.Activity;
25
import android.bluetooth.BluetoothAdapter;
26
import android.content.ActivityNotFoundException;
27
import android.content.BroadcastReceiver;
28
import android.content.ContentValues;
29
import android.content.Context;
30
import android.content.DialogInterface;
31
import android.content.Intent;
32
import android.content.IntentFilter;
33
import android.content.pm.PackageInfo;
34
import android.content.pm.PackageManager;
35
import android.content.pm.Signature;
36
import android.database.ContentObserver;
37
import android.graphics.Bitmap;
38
import android.net.Uri;
39
import android.os.Bundle;
40
import android.os.Handler;
41
import android.support.annotation.NonNull;
42
import android.support.annotation.Nullable;
43
import android.support.v4.app.Fragment;
44
import android.support.v4.app.ListFragment;
45
import android.support.v4.app.NavUtils;
46
import android.support.v4.content.LocalBroadcastManager;
47
import android.support.v4.view.MenuItemCompat;
48
import android.support.v7.app.AlertDialog;
49
import android.support.v7.app.AppCompatActivity;
50
import android.text.Html;
51
import android.text.Layout;
52
import android.text.Selection;
53
import android.text.Spannable;
54
import android.text.Spanned;
55
import android.text.TextUtils;
56
import android.text.format.DateFormat;
57
import android.text.method.LinkMovementMethod;
58
import android.text.style.ClickableSpan;
59
import android.util.Log;
60
import android.view.LayoutInflater;
61
import android.view.Menu;
62
import android.view.MenuItem;
63
import android.view.MotionEvent;
64
import android.view.View;
65
import android.view.ViewGroup;
66
import android.view.Window;
67
import android.widget.ArrayAdapter;
68
import android.widget.Button;
69
import android.widget.FrameLayout;
70
import android.widget.ImageButton;
71
import android.widget.ImageView;
72
import android.widget.LinearLayout;
73
import android.widget.ListView;
74
import android.widget.ProgressBar;
75
import android.widget.TextView;
76
import android.widget.Toast;
77

    
78
import com.nostra13.universalimageloader.core.DisplayImageOptions;
79
import com.nostra13.universalimageloader.core.ImageLoader;
80
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
81

    
82
import org.fdroid.fdroid.Utils.CommaSeparatedList;
83
import org.fdroid.fdroid.compat.PackageManagerCompat;
84
import org.fdroid.fdroid.data.Apk;
85
import org.fdroid.fdroid.data.ApkProvider;
86
import org.fdroid.fdroid.data.App;
87
import org.fdroid.fdroid.data.AppProvider;
88
import org.fdroid.fdroid.data.InstalledAppProvider;
89
import org.fdroid.fdroid.data.Repo;
90
import org.fdroid.fdroid.data.RepoProvider;
91
import org.fdroid.fdroid.installer.Installer;
92
import org.fdroid.fdroid.installer.Installer.AndroidNotCompatibleException;
93
import org.fdroid.fdroid.installer.Installer.InstallerCallback;
94
import org.fdroid.fdroid.net.ApkDownloader;
95
import org.fdroid.fdroid.net.AsyncDownloaderFromAndroid;
96
import org.fdroid.fdroid.net.Downloader;
97

    
98
import java.io.File;
99
import java.security.NoSuchAlgorithmException;
100
import java.text.DecimalFormat;
101
import java.util.Iterator;
102
import java.util.List;
103

    
104
interface AppDetailsData {
105
    App getApp();
106
    AppDetails.ApkListAdapter getApks();
107
    Signature getInstalledSignature();
108
    String getInstalledSignatureId();
109
}
110

    
111
/**
112
 * Interface which allows the apk list fragment to communicate with the activity when
113
 * a user requests to install/remove an apk by clicking on an item in the list.
114
 *
115
 * NOTE: This is <em>not</em> to do with with the sudo/packagemanager/other installer
116
 * stuff which allows multiple ways to install apps. It is only here to make fragment-
117
 * activity communication possible.
118
 */
119
interface AppInstallListener {
120
    void install(final Apk apk);
121
    void removeApk(String packageName);
122
}
123

    
124
public class AppDetails extends AppCompatActivity implements ProgressListener, AppDetailsData, AppInstallListener {
125

    
126
    private static final String TAG = "AppDetails";
127

    
128
    public static final int REQUEST_ENABLE_BLUETOOTH = 2;
129

    
130
    public static final String EXTRA_APPID = "appid";
131
    public static final String EXTRA_FROM = "from";
132

    
133
    private FDroidApp fdroidApp;
134
    private ApkListAdapter adapter;
135

    
136
    private static class ViewHolder {
137
        TextView version;
138
        TextView status;
139
        TextView size;
140
        TextView api;
141
        TextView incompatibleReasons;
142
        TextView buildtype;
143
        TextView added;
144
        TextView nativecode;
145
    }
146

    
147
    // observer to update view when package has been installed/deleted
148
    AppObserver myAppObserver;
149

    
150
    class AppObserver extends ContentObserver {
151

    
152
        public AppObserver(Handler handler) {
153
            super(handler);
154
        }
155

    
156
        @Override
157
        public void onChange(boolean selfChange) {
158
            onChange(selfChange, null);
159
        }
160

    
161
        @Override
162
        public void onChange(boolean selfChange, Uri uri) {
163
            onAppChanged();
164
        }
165

    
166
    }
167

    
168
    class ApkListAdapter extends ArrayAdapter<Apk> {
169

    
170
        private final LayoutInflater mInflater = (LayoutInflater) mctx.getSystemService(
171
                Context.LAYOUT_INFLATER_SERVICE);
172

    
173
        public ApkListAdapter(Context context, App app) {
174
            super(context, 0);
175
            final List<Apk> apks = ApkProvider.Helper.findByApp(context, app.id);
176
            for (final Apk apk : apks) {
177
                if (apk.compatible || Preferences.get().showIncompatibleVersions()) {
178
                    add(apk);
179
                }
180
            }
181
        }
182

    
183
        private String getInstalledStatus(final Apk apk) {
184
            // Definitely not installed.
185
            if (apk.vercode != app.installedVersionCode) {
186
                return getString(R.string.not_inst);
187
            }
188
            // Definitely installed this version.
189
            if (mInstalledSigID != null && apk.sig != null
190
                    && apk.sig.equals(mInstalledSigID)) {
191
                return getString(R.string.inst);
192
            }
193
            // Installed the same version, but from someplace else.
194
            final String installerPkgName;
195
            try {
196
                installerPkgName = mPm.getInstallerPackageName(app.id);
197
            } catch (IllegalArgumentException e) {
198
                Log.w(TAG, "Application " + app.id + " is not installed anymore");
199
                return getString(R.string.not_inst);
200
            }
201
            if (TextUtils.isEmpty(installerPkgName)) {
202
                return getString(R.string.inst_unknown_source);
203
            }
204
            final String installerLabel = InstalledAppProvider
205
                .getApplicationLabel(mctx, installerPkgName);
206
            return getString(R.string.inst_known_source, installerLabel);
207
        }
208

    
209
        @Override
210
        public View getView(int position, View convertView, ViewGroup parent) {
211

    
212
            java.text.DateFormat df = DateFormat.getDateFormat(mctx);
213
            final Apk apk = getItem(position);
214
            ViewHolder holder;
215

    
216
            if (convertView == null) {
217
                convertView = mInflater.inflate(R.layout.apklistitem, parent, false);
218

    
219
                holder = new ViewHolder();
220
                holder.version = (TextView) convertView.findViewById(R.id.version);
221
                holder.status = (TextView) convertView.findViewById(R.id.status);
222
                holder.size = (TextView) convertView.findViewById(R.id.size);
223
                holder.api = (TextView) convertView.findViewById(R.id.api);
224
                holder.incompatibleReasons = (TextView) convertView.findViewById(R.id.incompatible_reasons);
225
                holder.buildtype = (TextView) convertView.findViewById(R.id.buildtype);
226
                holder.added = (TextView) convertView.findViewById(R.id.added);
227
                holder.nativecode = (TextView) convertView.findViewById(R.id.nativecode);
228

    
229
                convertView.setTag(holder);
230
            } else {
231
                holder = (ViewHolder) convertView.getTag();
232
            }
233

    
234
            holder.version.setText(getString(R.string.version)
235
                    + " " + apk.version
236
                    + (apk.vercode == app.suggestedVercode ? "" : ""));
237

    
238
            holder.status.setText(getInstalledStatus(apk));
239

    
240
            if (apk.size > 0) {
241
                holder.size.setText(Utils.getFriendlySize(apk.size));
242
                holder.size.setVisibility(View.VISIBLE);
243
            } else {
244
                holder.size.setVisibility(View.GONE);
245
            }
246

    
247
            if (!Preferences.get().expertMode()) {
248
                holder.api.setVisibility(View.GONE);
249
            } else if (apk.minSdkVersion > 0 && apk.maxSdkVersion > 0) {
250
                holder.api.setText(getString(R.string.minsdk_up_to_maxsdk,
251
                            Utils.getAndroidVersionName(apk.minSdkVersion),
252
                            Utils.getAndroidVersionName(apk.maxSdkVersion)));
253
                holder.api.setVisibility(View.VISIBLE);
254
            } else if (apk.minSdkVersion > 0) {
255
                holder.api.setText(getString(R.string.minsdk_or_later,
256
                            Utils.getAndroidVersionName(apk.minSdkVersion)));
257
                holder.api.setVisibility(View.VISIBLE);
258
            } else if (apk.maxSdkVersion > 0) {
259
                holder.api.setText(getString(R.string.up_to_maxsdk,
260
                            Utils.getAndroidVersionName(apk.maxSdkVersion)));
261
                holder.api.setVisibility(View.VISIBLE);
262
            }
263

    
264
            if (apk.srcname != null) {
265
                holder.buildtype.setText("source");
266
            } else {
267
                holder.buildtype.setText("bin");
268
            }
269

    
270
            if (apk.added != null) {
271
                holder.added.setText(getString(R.string.added_on,
272
                            df.format(apk.added)));
273
                holder.added.setVisibility(View.VISIBLE);
274
            } else {
275
                holder.added.setVisibility(View.GONE);
276
            }
277

    
278
            if (Preferences.get().expertMode() && apk.nativecode != null) {
279
                holder.nativecode.setText(apk.nativecode.toString().replaceAll(","," "));
280
                holder.nativecode.setVisibility(View.VISIBLE);
281
            } else {
282
                holder.nativecode.setVisibility(View.GONE);
283
            }
284

    
285
            if (apk.incompatible_reasons != null) {
286
                holder.incompatibleReasons.setText(
287
                    getResources().getString(
288
                        R.string.requires_features,
289
                        apk.incompatible_reasons.toPrettyString()));
290
                holder.incompatibleReasons.setVisibility(View.VISIBLE);
291
            } else {
292
                holder.incompatibleReasons.setVisibility(View.GONE);
293
            }
294

    
295
            // Disable it all if it isn't compatible...
296
            final View[] views = {
297
                convertView,
298
                holder.version,
299
                holder.status,
300
                holder.size,
301
                holder.api,
302
                holder.buildtype,
303
                holder.added,
304
                holder.nativecode
305
            };
306

    
307
            for (final View v : views) {
308
                v.setEnabled(apk.compatible);
309
            }
310

    
311
            return convertView;
312
        }
313
    }
314

    
315
    private static final int INSTALL            = Menu.FIRST;
316
    private static final int UNINSTALL          = Menu.FIRST + 1;
317
    private static final int IGNOREALL          = Menu.FIRST + 2;
318
    private static final int IGNORETHIS         = Menu.FIRST + 3;
319
    private static final int LAUNCH             = Menu.FIRST + 4;
320
    private static final int SHARE              = Menu.FIRST + 5;
321
    private static final int SEND_VIA_BLUETOOTH = Menu.FIRST + 6;
322

    
323
    private App app;
324
    private PackageManager mPm;
325
    private ApkDownloader downloadHandler;
326
    private LocalBroadcastManager localBroadcastManager;
327

    
328
    private boolean startingIgnoreAll;
329
    private int startingIgnoreThis;
330

    
331
    private final Context mctx = this;
332
    private Installer installer;
333

    
334

    
335
    private AppDetailsHeaderFragment mHeaderFragment;
336

    
337
    /**
338
     * Stores relevant data that we want to keep track of when destroying the activity
339
     * with the expectation of it being recreated straight away (e.g. after an
340
     * orientation change). One of the major things is that we want the download thread
341
     * to stay active, but for it not to trigger any UI stuff (e.g. progress bar)
342
     * between the activity being destroyed and recreated.
343
     */
344
    private static class ConfigurationChangeHelper {
345

    
346
        public final ApkDownloader downloader;
347
        public final App app;
348

    
349
        public ConfigurationChangeHelper(ApkDownloader downloader, App app) {
350
            this.downloader = downloader;
351
            this.app = app;
352
        }
353
    }
354

    
355
    private boolean inProcessOfChangingConfiguration = false;
356

    
357
    /**
358
     * Attempt to extract the appId from the intent which launched this activity.
359
     * @return May return null, if we couldn't find the appId. This should
360
     * never happen as AppDetails is only to be called by the FDroid activity
361
     * and not externally.
362
     */
363
    private String getAppIdFromIntent() {
364
        Intent i = getIntent();
365
        if (!i.hasExtra(EXTRA_APPID)) {
366
            Log.e(TAG, "No application ID found in the intent!");
367
            return null;
368
        }
369

    
370
        return i.getStringExtra(EXTRA_APPID);
371
    }
372

    
373
    @Override
374
    protected void onCreate(Bundle savedInstanceState) {
375

    
376
        fdroidApp = ((FDroidApp) getApplication());
377
        fdroidApp.applyTheme(this);
378

    
379
        super.onCreate(savedInstanceState);
380

    
381
        // Must be called *after* super.onCreate(), as that is where the action bar
382
        // compat implementation is assigned in the ActionBarActivity base class.
383
        supportRequestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
384

    
385
        if (getIntent().hasExtra(EXTRA_FROM)) {
386
            setTitle(getIntent().getStringExtra(EXTRA_FROM));
387
        }
388

    
389
        mPm = getPackageManager();
390

    
391
        installer = Installer.getActivityInstaller(this, mPm, myInstallerCallback);
392

    
393
        // Get the preferences we're going to use in this Activity...
394
        ConfigurationChangeHelper previousData = (ConfigurationChangeHelper)getLastCustomNonConfigurationInstance();
395
        if (previousData != null) {
396
            Utils.DebugLog(TAG, "Recreating view after configuration change.");
397
            downloadHandler = previousData.downloader;
398
            if (downloadHandler != null) {
399
                Utils.DebugLog(TAG, "Download was in progress before the configuration change, so we will start to listen to its events again.");
400
            }
401
            app = previousData.app;
402
            setApp(app);
403
        } else {
404
            if (!reset(getAppIdFromIntent())) {
405
                finish();
406
                return;
407
            }
408
        }
409

    
410
        // Set up the list...
411
        adapter = new ApkListAdapter(this, app);
412

    
413
        // Wait until all other intialization before doing this, because it will create the
414
        // fragments, which rely on data from the activity that is set earlier in this method.
415
        setContentView(R.layout.app_details);
416

    
417
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
418

    
419
        // Check for the presence of a view which only exists in the landscape view.
420
        // This seems to be the preferred way to interrogate the view, rather than
421
        // to check the orientation. I guess this is because views can be dynamically
422
        // chosen based on more than just orientation (e.g. large screen sizes).
423
        View onlyInLandscape = findViewById(R.id.app_summary_container);
424

    
425
        AppDetailsListFragment listFragment =
426
                (AppDetailsListFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_app_list);
427
        if (onlyInLandscape == null) {
428
            listFragment.setupSummaryHeader();
429
        } else {
430
            listFragment.removeSummaryHeader();
431
        }
432

    
433
        localBroadcastManager = LocalBroadcastManager.getInstance(this);
434

    
435
        // Check if a download is running for this app
436
        if (AsyncDownloaderFromAndroid.isDownloading(this, app.id) >= 0) {
437
            // call install() to re-setup the listeners and downloaders
438
            // the AsyncDownloader will not restart the download since the download is running,
439
            // and thus the version we pass to install() is not important
440
            refreshHeader();
441
            refreshApkList();
442
            final Apk apkToInstall = ApkProvider.Helper.find(this, app.id, app.suggestedVercode);
443
            install(apkToInstall);
444
        }
445

    
446
    }
447

    
448
    // The signature of the installed version.
449
    private Signature mInstalledSignature;
450
    private String mInstalledSigID;
451

    
452
    @Override
453
    protected void onStart() {
454
        super.onStart();
455
        // register observer to know when install status changes
456
        myAppObserver = new AppObserver(new Handler());
457
        getContentResolver().registerContentObserver(
458
                AppProvider.getContentUri(app.id),
459
                true,
460
                myAppObserver);
461
    }
462

    
463
    @Override
464
    protected void onResumeFragments() {
465
        super.onResumeFragments();
466
        refreshApkList();
467
        refreshHeader();
468
        supportInvalidateOptionsMenu();
469

    
470
        if (downloadHandler != null) {
471
            if (downloadHandler.isComplete()) {
472
                downloadCompleteInstallApk();
473
            } else {
474
                localBroadcastManager.registerReceiver(downloaderProgressReceiver,
475
                        new IntentFilter(Downloader.LOCAL_ACTION_PROGRESS));
476
                downloadHandler.setProgressListener(this);
477

    
478
                if (downloadHandler.getTotalBytes() == 0)
479
                    mHeaderFragment.startProgress();
480
                else
481
                    mHeaderFragment.updateProgress(downloadHandler.getBytesRead(), downloadHandler.getTotalBytes());
482
            }
483
        }
484
    }
485

    
486
    /**
487
     * Remove progress listener, suppress progress bar, set downloadHandler to null.
488
     */
489
    private void cleanUpFinishedDownload() {
490
        if (downloadHandler != null) {
491
            downloadHandler.removeProgressListener();
492
            mHeaderFragment.removeProgress();
493
            downloadHandler = null;
494
        }
495
    }
496

    
497
    /**
498
     * Once the download completes successfully, call this method to start the install process
499
     * with the file that was downloaded.
500
     */
501
    private void downloadCompleteInstallApk() {
502
        if (downloadHandler != null) {
503
            installApk(downloadHandler.localFile());
504
            cleanUpFinishedDownload();
505
        }
506
    }
507

    
508
    protected void onStop() {
509
        super.onStop();
510
        getContentResolver().unregisterContentObserver(myAppObserver);
511
    }
512

    
513
    @Override
514
    protected void onPause() {
515
        super.onPause();
516
        if (app != null && (app.ignoreAllUpdates != startingIgnoreAll
517
                || app.ignoreThisUpdate != startingIgnoreThis)) {
518
            Utils.DebugLog(TAG, "Updating 'ignore updates', as it has changed since we started the activity...");
519
            setIgnoreUpdates(app.id, app.ignoreAllUpdates, app.ignoreThisUpdate);
520
        }
521

    
522
        localBroadcastManager.unregisterReceiver(downloaderProgressReceiver);
523
        if (downloadHandler != null) {
524
            downloadHandler.removeProgressListener();
525
        }
526

    
527
        mHeaderFragment.removeProgress();
528
    }
529

    
530
    private final BroadcastReceiver downloaderProgressReceiver = new BroadcastReceiver() {
531
        @Override
532
        public void onReceive(Context context, Intent intent) {
533
            if (mHeaderFragment != null)
534
                mHeaderFragment.updateProgress(intent.getIntExtra(Downloader.EXTRA_BYTES_READ, -1),
535
                    intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, -1));
536
        }
537
    };
538

    
539
    private void onAppChanged() {
540
        if (!reset(app.id)) {
541
            AppDetails.this.finish();
542
            return;
543
        }
544

    
545
        refreshApkList();
546
        refreshHeader();
547
        supportInvalidateOptionsMenu();
548
    }
549

    
550
    public void setIgnoreUpdates(String appId, boolean ignoreAll, int ignoreVersionCode) {
551

    
552
        Uri uri = AppProvider.getContentUri(appId);
553

    
554
        ContentValues values = new ContentValues(2);
555
        values.put(AppProvider.DataColumns.IGNORE_ALLUPDATES, ignoreAll ? 1 : 0);
556
        values.put(AppProvider.DataColumns.IGNORE_THISUPDATE, ignoreVersionCode);
557

    
558
        getContentResolver().update(uri, values, null, null);
559

    
560
    }
561

    
562

    
563
    @Override
564
    public Object onRetainCustomNonConfigurationInstance() {
565
        inProcessOfChangingConfiguration = true;
566
        return new ConfigurationChangeHelper(downloadHandler, app);
567
    }
568

    
569
    @Override
570
    protected void onDestroy() {
571
        if (downloadHandler != null) {
572
            if (!inProcessOfChangingConfiguration) {
573
                downloadHandler.cancel(false);
574
                cleanUpFinishedDownload();
575
            }
576
        }
577
        inProcessOfChangingConfiguration = false;
578
        super.onDestroy();
579
    }
580

    
581
    // Reset the display and list contents. Used when entering the activity, and
582
    // also when something has been installed/uninstalled.
583
    // Return true if the app was found, false otherwise.
584
    private boolean reset(String appId) {
585

    
586
        Utils.DebugLog(TAG, "Getting application details for " + appId);
587
        App newApp = null;
588

    
589
        if (!TextUtils.isEmpty(appId)) {
590
            newApp = AppProvider.Helper.findById(getContentResolver(), appId);
591
        }
592

    
593
        setApp(newApp);
594

    
595
        return this.app != null;
596
    }
597

    
598
    /**
599
     * If passed null, this will show a message to the user ("Could not find app ..." or something
600
     * like that) and then finish the activity.
601
     */
602
    private void setApp(App newApp) {
603

    
604
        if (newApp == null) {
605
            Toast.makeText(this, R.string.no_such_app, Toast.LENGTH_LONG).show();
606
            finish();
607
            return;
608
        }
609

    
610
        app = newApp;
611

    
612
        startingIgnoreAll = app.ignoreAllUpdates;
613
        startingIgnoreThis = app.ignoreThisUpdate;
614

    
615
        // Get the signature of the installed package...
616
        mInstalledSignature = null;
617
        mInstalledSigID = null;
618

    
619
        if (app.isInstalled()) {
620
            PackageManager pm = getPackageManager();
621
            try {
622
                PackageInfo pi = pm.getPackageInfo(app.id, PackageManager.GET_SIGNATURES);
623
                mInstalledSignature = pi.signatures[0];
624
                Hasher hash = new Hasher("MD5", mInstalledSignature.toCharsString().getBytes());
625
                mInstalledSigID = hash.getHash();
626
            } catch (PackageManager.NameNotFoundException e) {
627
                Log.w(TAG, "Failed to get installed signature");
628
            } catch (NoSuchAlgorithmException e) {
629
                Log.w(TAG, "Failed to calculate signature MD5 sum");
630
                mInstalledSignature = null;
631
            }
632
        }
633
    }
634

    
635
    private void refreshApkList() {
636
        adapter.notifyDataSetChanged();
637
    }
638

    
639
    private void refreshHeader() {
640
        mHeaderFragment = (AppDetailsHeaderFragment)
641
                getSupportFragmentManager().findFragmentById(R.id.header);
642
        mHeaderFragment.updateViews();
643
    }
644

    
645
    @Override
646
    public boolean onPrepareOptionsMenu(Menu menu) {
647
        super.onPrepareOptionsMenu(menu);
648
        menu.clear();
649
        if (app == null)
650
            return true;
651

    
652
        MenuItemCompat.setShowAsAction(menu.add(
653
                        Menu.NONE, SHARE, 1, R.string.menu_share)
654
                        .setIcon(R.drawable.ic_share_white),
655
                MenuItemCompat.SHOW_AS_ACTION_IF_ROOM |
656
                        MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
657

    
658
        if (app.isInstalled()) {
659
            MenuItemCompat.setShowAsAction(menu.add(
660
                            Menu.NONE, UNINSTALL, 1, R.string.menu_uninstall)
661
                            .setIcon(R.drawable.ic_delete_white),
662
                    MenuItemCompat.SHOW_AS_ACTION_IF_ROOM |
663
                            MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
664
        }
665

    
666
        if (mPm.getLaunchIntentForPackage(app.id) != null && app.canAndWantToUpdate()) {
667
            MenuItemCompat.setShowAsAction(menu.add(
668
                            Menu.NONE, LAUNCH, 1, R.string.menu_launch)
669
                            .setIcon(R.drawable.ic_play_arrow_white),
670
                    MenuItemCompat.SHOW_AS_ACTION_ALWAYS |
671
                            MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
672
        }
673

    
674
        menu.add(Menu.NONE, IGNOREALL, 2, R.string.menu_ignore_all)
675
                    .setIcon(R.drawable.ic_do_not_disturb_white)
676
                    .setCheckable(true)
677
                    .setChecked(app.ignoreAllUpdates);
678

    
679
        if (app.hasUpdates()) {
680
            menu.add(Menu.NONE, IGNORETHIS, 2, R.string.menu_ignore_this)
681
                    .setIcon(R.drawable.ic_do_not_disturb_white)
682
                    .setCheckable(true)
683
                    .setChecked(app.ignoreThisUpdate >= app.suggestedVercode);
684
        }
685

    
686
        // Ignore on devices without Bluetooth
687
        if (app.isInstalled() && fdroidApp.bluetoothAdapter != null) {
688
            menu.add(Menu.NONE, SEND_VIA_BLUETOOTH, 3, R.string.send_via_bluetooth)
689
                    .setIcon(R.drawable.ic_bluetooth_white);
690
        }
691
        return true;
692
    }
693

    
694

    
695
    private void tryOpenUri(String s) {
696
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(s));
697
        if (intent.resolveActivity(getPackageManager()) == null) {
698
            Toast.makeText(this,
699
                    getString(R.string.no_handler_app, intent.getDataString()),
700
                    Toast.LENGTH_LONG).show();
701
            return;
702
        }
703
        startActivity(intent);
704
    }
705

    
706
    private static class SafeLinkMovementMethod extends LinkMovementMethod {
707

    
708
        private static SafeLinkMovementMethod instance;
709

    
710
        private final Context ctx;
711

    
712
        private SafeLinkMovementMethod(Context ctx) {
713
            this.ctx = ctx;
714
        }
715

    
716
        public static SafeLinkMovementMethod getInstance(Context ctx) {
717
            if (instance == null) {
718
                instance = new SafeLinkMovementMethod(ctx);
719
            }
720
            return instance;
721
        }
722

    
723
        private static CharSequence getLink(TextView widget, Spannable buffer,
724
                MotionEvent event) {
725
            int x = (int) event.getX();
726
            int y = (int) event.getY();
727
            x -= widget.getTotalPaddingLeft();
728
            y -= widget.getTotalPaddingTop();
729
            x += widget.getScrollX();
730
            y += widget.getScrollY();
731

    
732
            Layout layout = widget.getLayout();
733
            final int line = layout.getLineForVertical(y);
734
            final int off = layout.getOffsetForHorizontal(line, x);
735
            final ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class);
736

    
737
            if (links.length > 0) {
738
                final ClickableSpan link = links[0];
739
                final Spanned s = (Spanned) widget.getText();
740
                return s.subSequence(s.getSpanStart(link), s.getSpanEnd(link));
741
            }
742
            return "null";
743
        }
744

    
745
        @Override
746
        public boolean onTouchEvent(@NonNull TextView widget, @NonNull Spannable buffer,
747
                @NonNull MotionEvent event) {
748
            try {
749
                return super.onTouchEvent(widget, buffer, event);
750
            } catch (ActivityNotFoundException ex) {
751
                Selection.removeSelection(buffer);
752
                final CharSequence link = getLink(widget, buffer, event);
753
                Toast.makeText(ctx,
754
                        ctx.getString(R.string.no_handler_app, link),
755
                        Toast.LENGTH_LONG).show();
756
                return true;
757
            }
758
        }
759

    
760
    }
761

    
762
    protected void navigateUp() {
763
        NavUtils.navigateUpFromSameTask(this);
764
    }
765

    
766
    @Override
767
    public boolean onOptionsItemSelected(MenuItem item) {
768

    
769
        switch (item.getItemId()) {
770

    
771
        case android.R.id.home:
772
            navigateUp();
773
            return true;
774

    
775
        case LAUNCH:
776
            launchApk(app.id);
777
            return true;
778

    
779
        case SHARE:
780
            shareApp(app);
781
            return true;
782

    
783
        case INSTALL:
784
            // Note that this handles updating as well as installing.
785
            if (app.suggestedVercode > 0) {
786
                final Apk apkToInstall = ApkProvider.Helper.find(this, app.id, app.suggestedVercode);
787
                install(apkToInstall);
788
            }
789
            return true;
790

    
791
        case UNINSTALL:
792
            removeApk(app.id);
793
            return true;
794

    
795
        case IGNOREALL:
796
            app.ignoreAllUpdates ^= true;
797
            item.setChecked(app.ignoreAllUpdates);
798
            return true;
799

    
800
        case IGNORETHIS:
801
            if (app.ignoreThisUpdate >= app.suggestedVercode)
802
                app.ignoreThisUpdate = 0;
803
            else
804
                app.ignoreThisUpdate = app.suggestedVercode;
805
            item.setChecked(app.ignoreThisUpdate > 0);
806
            return true;
807

    
808
        case SEND_VIA_BLUETOOTH:
809
            /*
810
             * If Bluetooth has not been enabled/turned on, then
811
             * enabling device discoverability will automatically enable Bluetooth
812
             */
813
            Intent discoverBt = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
814
            discoverBt.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 121);
815
            startActivityForResult(discoverBt, REQUEST_ENABLE_BLUETOOTH);
816
            // if this is successful, the Bluetooth transfer is started
817
            return true;
818

    
819
        }
820
        return super.onOptionsItemSelected(item);
821
    }
822

    
823
    // Install the version of this app denoted by 'app.curApk'.
824
    @Override
825
    public void install(final Apk apk) {
826
        // Ignore call if another download is running.
827
        if (downloadHandler != null && !downloadHandler.isComplete())
828
            return;
829

    
830
        final String repoaddress = getRepoAddress(apk);
831
        if (repoaddress == null) return;
832

    
833
        if (!apk.compatible) {
834
            AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this);
835
            ask_alrt.setMessage(R.string.installIncompatible);
836
            ask_alrt.setPositiveButton(R.string.yes,
837
                    new DialogInterface.OnClickListener() {
838
                        @Override
839
                        public void onClick(DialogInterface dialog,
840
                                int whichButton) {
841
                            startDownload(apk, repoaddress);
842
                        }
843
                    });
844
            ask_alrt.setNegativeButton(R.string.no,
845
                    new DialogInterface.OnClickListener() {
846
                        @Override
847
                        public void onClick(DialogInterface dialog,
848
                                int whichButton) {
849
                        }
850
                    });
851
            AlertDialog alert = ask_alrt.create();
852
            alert.show();
853
            return;
854
        }
855
        if (mInstalledSigID != null && apk.sig != null
856
                && !apk.sig.equals(mInstalledSigID)) {
857
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
858
            builder.setMessage(R.string.SignatureMismatch).setPositiveButton(
859
                    R.string.ok,
860
                    new DialogInterface.OnClickListener() {
861
                        @Override
862
                        public void onClick(DialogInterface dialog, int id) {
863
                            dialog.cancel();
864
                        }
865
                    });
866
            AlertDialog alert = builder.create();
867
            alert.show();
868
            return;
869
        }
870
        startDownload(apk, repoaddress);
871
    }
872

    
873
    @Nullable
874
    private String getRepoAddress(Apk apk) {
875
        final String[] projection = { RepoProvider.DataColumns.ADDRESS };
876
        Repo repo = RepoProvider.Helper.findById(this, apk.repo, projection);
877
        if (repo == null || repo.address == null) {
878
            return null;
879
        }
880
        return repo.address;
881
    }
882

    
883
    private void startDownload(Apk apk, String repoAddress) {
884
        downloadHandler = new ApkDownloader(getBaseContext(), app, apk, repoAddress);
885
        localBroadcastManager.registerReceiver(downloaderProgressReceiver,
886
                new IntentFilter(Downloader.LOCAL_ACTION_PROGRESS));
887
        downloadHandler.setProgressListener(this);
888
        if (downloadHandler.download()) {
889
            mHeaderFragment.startProgress();
890
        }
891
    }
892

    
893
    private void installApk(File file) {
894
        try {
895
            installer.installPackage(file);
896
        } catch (AndroidNotCompatibleException e) {
897
            Log.e(TAG, "Android not compatible with this Installer!", e);
898
        }
899
    }
900

    
901
    @Override
902
    public void removeApk(String packageName) {
903
        try {
904
            installer.deletePackage(packageName);
905
        } catch (AndroidNotCompatibleException e) {
906
            Log.e(TAG, "Android not compatible with this Installer!", e);
907
        }
908
    }
909

    
910
    final Installer.InstallerCallback myInstallerCallback = new Installer.InstallerCallback() {
911

    
912
        @Override
913
        public void onSuccess(final int operation) {
914
            runOnUiThread(new Runnable() {
915
                @Override
916
                public void run() {
917
                    if (operation == Installer.InstallerCallback.OPERATION_INSTALL) {
918
                        PackageManagerCompat.setInstaller(mPm, app.id);
919
                    }
920

    
921
                    onAppChanged();
922
                }
923
            });
924
        }
925

    
926
        @Override
927
        public void onError(int operation, final int errorCode) {
928
            if (errorCode == InstallerCallback.ERROR_CODE_CANCELED) {
929
                return;
930
            }
931
            final int title, body;
932
            if (operation == InstallerCallback.OPERATION_INSTALL) {
933
                title = R.string.install_error_title;
934
                switch (errorCode) {
935
                case ERROR_CODE_CANNOT_PARSE:
936
                    body = R.string.install_error_cannot_parse;
937
                    break;
938
                default: // ERROR_CODE_OTHER
939
                    body = R.string.install_error_unknown;
940
                    break;
941
                }
942
            } else { // InstallerCallback.OPERATION_DELETE
943
                title = R.string.uninstall_error_title;
944
                switch (errorCode) {
945
                default: // ERROR_CODE_OTHER
946
                    body = R.string.install_error_unknown;
947
                    break;
948
                }
949
            }
950
            runOnUiThread(new Runnable() {
951
                @Override
952
                public void run() {
953
                    onAppChanged();
954

    
955
                    Log.e(TAG, "Installer aborted with errorCode: " + errorCode);
956

    
957
                    AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this);
958
                    alertBuilder.setTitle(title);
959
                    alertBuilder.setMessage(body);
960
                    alertBuilder.setNeutralButton(android.R.string.ok, null);
961
                    alertBuilder.create().show();
962
                }
963
            });
964
        }
965
    };
966

    
967
    private void launchApk(String id) {
968
        Intent intent = mPm.getLaunchIntentForPackage(id);
969
        startActivity(intent);
970
    }
971

    
972
    private void shareApp(App app) {
973
        Intent shareIntent = new Intent(Intent.ACTION_SEND);
974
        shareIntent.setType("text/plain");
975

    
976
        shareIntent.putExtra(Intent.EXTRA_SUBJECT, app.name);
977
        shareIntent.putExtra(Intent.EXTRA_TEXT, app.name + " (" + app.summary + ") - https://f-droid.org/app/" + app.id);
978

    
979
        startActivity(Intent.createChooser(shareIntent, getString(R.string.menu_share)));
980
    }
981

    
982
    @Override
983
    public void onProgress(Event event) {
984
        if (downloadHandler == null || !downloadHandler.isEventFromThis(event)) {
985
            // Choose not to respond to events from previous downloaders.
986
            // We don't even care if we receive "cancelled" events or the like, because
987
            // we dealt with cancellations in the onCancel listener of the dialog,
988
            // rather than waiting to receive the event here. We try and be careful in
989
            // the download thread to make sure that we check for cancellations before
990
            // sending events, but it is not possible to be perfect, because the interruption
991
            // which triggers the download can happen after the check to see if
992
            Utils.DebugLog(TAG, "Discarding downloader event \"" + event.type + "\" as it is from an old (probably cancelled) downloader.");
993
            return;
994
        }
995

    
996
        boolean finished = false;
997
        switch (event.type) {
998
        case ApkDownloader.EVENT_ERROR:
999
            final int res;
1000
            if (event.getData().getInt(ApkDownloader.EVENT_DATA_ERROR_TYPE) == ApkDownloader.ERROR_HASH_MISMATCH)
1001
                res = R.string.corrupt_download;
1002
            else
1003
                res = R.string.details_notinstalled;
1004
            // this must be on the main UI thread
1005
            Toast.makeText(this, res, Toast.LENGTH_LONG).show();
1006
            cleanUpFinishedDownload();
1007
            finished = true;
1008
            break;
1009
        case ApkDownloader.EVENT_APK_DOWNLOAD_COMPLETE:
1010
            downloadCompleteInstallApk();
1011
            finished = true;
1012
            break;
1013
        }
1014

    
1015
        if (finished) {
1016
            if (mHeaderFragment != null)
1017
                mHeaderFragment.removeProgress();
1018
            downloadHandler = null;
1019
        }
1020
    }
1021

    
1022
    @Override
1023
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
1024
        // handle cases for install manager first
1025
        if (installer.handleOnActivityResult(requestCode, resultCode, data)) {
1026
            return;
1027
        }
1028

    
1029
        switch (requestCode) {
1030
        case REQUEST_ENABLE_BLUETOOTH:
1031
            fdroidApp.sendViaBluetooth(this, resultCode, app.id);
1032
            break;
1033
        }
1034
    }
1035

    
1036
    @Override
1037
    public App getApp() { return app; }
1038

    
1039
    @Override
1040
    public ApkListAdapter getApks() { return adapter; }
1041

    
1042
    @Override
1043
    public Signature getInstalledSignature() { return mInstalledSignature; }
1044

    
1045
    @Override
1046
    public String getInstalledSignatureId() { return mInstalledSigID; }
1047

    
1048
    public static class AppDetailsSummaryFragment extends Fragment {
1049

    
1050
        protected final Preferences prefs;
1051
        private AppDetailsData data;
1052
        private static final int MAX_LINES = 5;
1053
        private static boolean view_all_description;
1054
        private static boolean view_all_information;
1055
        private static boolean view_all_permissions;
1056
        private static LinearLayout ll_view_more_description;
1057
        private static LinearLayout ll_view_more_information;
1058
        private static LinearLayout ll_view_more_permissions;
1059

    
1060
        public AppDetailsSummaryFragment() {
1061
            prefs = Preferences.get();
1062
        }
1063

    
1064
        @Override
1065
        public void onAttach(Activity activity) {
1066
            super.onAttach(activity);
1067
            data = (AppDetailsData)activity;
1068
        }
1069

    
1070
        protected App getApp() { return data.getApp(); }
1071

    
1072
        protected ApkListAdapter getApks() { return data.getApks(); }
1073

    
1074
        protected Signature getInstalledSignature() { return data.getInstalledSignature(); }
1075

    
1076
        protected String getInstalledSignatureId() { return data.getInstalledSignatureId(); }
1077

    
1078
        @Override
1079
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
1080
            super.onCreateView(inflater, container, savedInstanceState);
1081
            View summaryView = inflater.inflate(R.layout.app_details_summary, container, false);
1082
            setupView(summaryView);
1083
            return summaryView;
1084
        }
1085

    
1086
        @Override
1087
        public void onResume() {
1088
            super.onResume();
1089
            updateViews(getView());
1090
        }
1091

    
1092
        private void setupView(final View view) {
1093
            // Expandable description
1094
            final TextView description = (TextView) view.findViewById(R.id.description);
1095
            final Spanned desc = Html.fromHtml(getApp().description, null, new Utils.HtmlTagHandler());
1096
            description.setMovementMethod(SafeLinkMovementMethod.getInstance(getActivity()));
1097
            description.setText(desc.subSequence(0, desc.length() - 2));
1098
            final ImageView view_more_description = (ImageView) view.findViewById(R.id.view_more_description);
1099
            description.post(new Runnable() {
1100
                @Override
1101
                public void run() {
1102
                    // If description has more than five lines
1103
                    if (description.getLineCount() > MAX_LINES) {
1104
                        description.setMaxLines(MAX_LINES);
1105
                        description.setOnClickListener(expander_description);
1106
                        view_all_description = true;
1107

    
1108
                        ll_view_more_description = (LinearLayout) view.findViewById(R.id.ll_description);
1109
                        ll_view_more_description.setOnClickListener(expander_description);
1110

    
1111
                        view_more_description.setImageResource(R.drawable.ic_expand_more_grey600);
1112
                        view_more_description.setOnClickListener(expander_description);
1113
                    } else {
1114
                        view_more_description.setVisibility(View.GONE);
1115
                    }
1116
                }
1117
            });
1118

    
1119
            // App ID
1120
            final TextView appIdView = (TextView) view.findViewById(R.id.appid);
1121
            if (prefs.expertMode())
1122
                appIdView.setText(getApp().id);
1123
            else
1124
                appIdView.setVisibility(View.GONE);
1125

    
1126
            // Expandable information
1127
            ll_view_more_information = (LinearLayout) view.findViewById(R.id.ll_information);
1128
            final TextView information = (TextView) view.findViewById(R.id.information);
1129
            final LinearLayout ll_view_more_information_content = (LinearLayout) view.findViewById(R.id.ll_information_content);
1130
            ll_view_more_information_content.setVisibility(View.GONE);
1131
            view_all_information = true;
1132
            information.setCompoundDrawablesWithIntrinsicBounds(null, null, getActivity().getResources().getDrawable(R.drawable.ic_expand_more_grey600), null);
1133

    
1134
            ll_view_more_information.setOnClickListener(expander_information);
1135
            information.setOnClickListener(expander_information);
1136

    
1137
            // Summary
1138
            final TextView summaryView = (TextView) view.findViewById(R.id.summary);
1139
            summaryView.setText(getApp().summary);
1140

    
1141
            // Website button
1142
            TextView tv = (TextView) view.findViewById(R.id.website);
1143
            if (!TextUtils.isEmpty(getApp().webURL))
1144
                tv.setOnClickListener(mOnClickListener);
1145
            else
1146
                tv.setVisibility(View.GONE);
1147

    
1148
            // Source button
1149
            tv = (TextView) view.findViewById(R.id.source);
1150
            if (!TextUtils.isEmpty(getApp().sourceURL))
1151
                tv.setOnClickListener(mOnClickListener);
1152
            else
1153
                tv.setVisibility(View.GONE);
1154

    
1155
            // Issues button
1156
            tv = (TextView) view.findViewById(R.id.issues);
1157
            if (!TextUtils.isEmpty(getApp().trackerURL))
1158
                tv.setOnClickListener(mOnClickListener);
1159
            else
1160
                tv.setVisibility(View.GONE);
1161

    
1162
            // Changelog button
1163
            tv = (TextView) view.findViewById(R.id.changelog);
1164
            if (!TextUtils.isEmpty(getApp().changelogURL))
1165
                tv.setOnClickListener(mOnClickListener);
1166
            else
1167
                tv.setVisibility(View.GONE);
1168

    
1169
            // Donate button
1170
            tv = (TextView) view.findViewById(R.id.donate);
1171
            if (!TextUtils.isEmpty(getApp().donateURL))
1172
                tv.setOnClickListener(mOnClickListener);
1173
            else
1174
                tv.setVisibility(View.GONE);
1175

    
1176
            // Bitcoin
1177
            tv = (TextView) view.findViewById(R.id.bitcoin);
1178
            if (!TextUtils.isEmpty(getApp().bitcoinAddr))
1179
                tv.setOnClickListener(mOnClickListener);
1180
            else
1181
                tv.setVisibility(View.GONE);
1182

    
1183
            // Litecoin
1184
            tv = (TextView) view.findViewById(R.id.litecoin);
1185
            if (!TextUtils.isEmpty(getApp().litecoinAddr))
1186
                tv.setOnClickListener(mOnClickListener);
1187
            else
1188
                tv.setVisibility(View.GONE);
1189

    
1190
            // Dogecoin
1191
            tv = (TextView) view.findViewById(R.id.dogecoin);
1192
            if (!TextUtils.isEmpty(getApp().dogecoinAddr))
1193
                tv.setOnClickListener(mOnClickListener);
1194
            else
1195
                tv.setVisibility(View.GONE);
1196

    
1197
            // Flattr
1198
            tv = (TextView) view.findViewById(R.id.flattr);
1199
            if (!TextUtils.isEmpty(getApp().flattrID))
1200
                tv.setOnClickListener(mOnClickListener);
1201
            else
1202
                tv.setVisibility(View.GONE);
1203

    
1204
            // Categories TextView
1205
            final TextView categories = (TextView) view.findViewById(R.id.categories);
1206
            if (prefs.expertMode() && getApp().categories != null)
1207
                categories.setText(getApp().categories.toString().replaceAll(",", ", "));
1208
            else
1209
                categories.setVisibility(View.GONE);
1210

    
1211
            Apk curApk = null;
1212
            for (int i = 0; i < getApks().getCount(); i++) {
1213
                final Apk apk = getApks().getItem(i);
1214
                if (apk.vercode == getApp().suggestedVercode) {
1215
                    curApk = apk;
1216
                    break;
1217
                }
1218
            }
1219

    
1220
            // Expandable permissions
1221
            ll_view_more_permissions = (LinearLayout) view.findViewById(R.id.ll_permissions);
1222
            final TextView permissionHeader = (TextView) view.findViewById(R.id.permissions);
1223
            final TextView permissionListView = (TextView) view.findViewById(R.id.permissions_list);
1224
            permissionListView.setVisibility(View.GONE);
1225
            view_all_permissions = true;
1226

    
1227
            final boolean curApkCompatible = curApk != null && curApk.compatible;
1228
            if (!getApks().isEmpty() && (curApkCompatible || prefs.showIncompatibleVersions())) {
1229
                permissionHeader.setText(getString(R.string.permissions_for_long, getApks().getItem(0).version));
1230
                permissionHeader.setCompoundDrawablesWithIntrinsicBounds(null, null, getActivity().getResources().getDrawable(R.drawable.ic_expand_more_grey600), null);
1231

    
1232
                ll_view_more_permissions.setOnClickListener(expander_permissions);
1233
                permissionHeader.setOnClickListener(expander_permissions);
1234
            } else {
1235
                permissionHeader.setVisibility(View.GONE);
1236
                permissionHeader.setCompoundDrawables(null, null, null, null);
1237
            }
1238

    
1239
            // Anti features
1240
            final TextView antiFeaturesView = (TextView) view.findViewById(R.id.antifeatures);
1241
            if (getApp().antiFeatures != null) {
1242
                StringBuilder sb = new StringBuilder();
1243
                for (final String af : getApp().antiFeatures) {
1244
                    final String afdesc = descAntiFeature(af);
1245
                    if (afdesc != null) {
1246
                        sb.append("\t").append(afdesc).append('\n');
1247
                    }
1248
                }
1249
                if (sb.length() > 0) {
1250
                    sb.setLength(sb.length() - 1);
1251
                    antiFeaturesView.setText(sb.toString());
1252
                } else {
1253
                    antiFeaturesView.setVisibility(View.GONE);
1254
                }
1255
            } else {
1256
                antiFeaturesView.setVisibility(View.GONE);
1257
            }
1258

    
1259
            updateViews(view);
1260
        }
1261

    
1262
        private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
1263
            public void onClick(View v) {
1264
                switch (v.getId()) {
1265
                    case R.id.website:
1266
                        ((AppDetails) getActivity()).tryOpenUri(getApp().webURL);
1267
                        break;
1268

    
1269
                    case R.id.source:
1270
                        ((AppDetails) getActivity()).tryOpenUri(getApp().sourceURL);
1271
                        break;
1272

    
1273
                    case R.id.issues:
1274
                        ((AppDetails) getActivity()).tryOpenUri(getApp().trackerURL);
1275
                        break;
1276

    
1277
                    case R.id.changelog:
1278
                        ((AppDetails) getActivity()).tryOpenUri(getApp().changelogURL);
1279
                        break;
1280

    
1281
                    case R.id.donate:
1282
                        ((AppDetails) getActivity()).tryOpenUri(getApp().donateURL);
1283
                        break;
1284

    
1285
                    case R.id.bitcoin:
1286
                        ((AppDetails) getActivity()).tryOpenUri("bitcoin:" + getApp().bitcoinAddr);
1287
                        break;
1288

    
1289
                    case R.id.litecoin:
1290
                        ((AppDetails) getActivity()).tryOpenUri("litecoin:" + getApp().litecoinAddr);
1291
                        break;
1292

    
1293
                    case R.id.dogecoin:
1294
                        ((AppDetails) getActivity()).tryOpenUri("dogecoin:" + getApp().dogecoinAddr);
1295
                        break;
1296

    
1297
                    case R.id.flattr:
1298
                        ((AppDetails) getActivity()).tryOpenUri("https://flattr.com/thing/" + getApp().flattrID);
1299
                        break;
1300
                }
1301
            }
1302
        };
1303

    
1304
        private final View.OnClickListener expander_description = new View.OnClickListener() {
1305
            public void onClick(View v) {
1306
                final TextView description = (TextView) ll_view_more_description.findViewById(R.id.description);
1307
                final ImageView view_more_permissions = (ImageView) ll_view_more_description.findViewById(R.id.view_more_description);
1308
                if (!view_all_description) {
1309
                    view_all_description = true;
1310
                    description.setMaxLines(MAX_LINES);
1311
                    view_more_permissions.setImageResource(R.drawable.ic_expand_more_grey600);
1312
                } else {
1313
                    view_all_description = false;
1314
                    description.setMaxLines(Integer.MAX_VALUE);
1315
                    view_more_permissions.setImageResource(R.drawable.ic_expand_less_grey600);
1316
                }
1317
            }
1318
        };
1319

    
1320
        private final View.OnClickListener expander_information = new View.OnClickListener() {
1321
            public void onClick(View v) {
1322
                final TextView informationHeader = (TextView) ll_view_more_information.findViewById(R.id.information);
1323
                final LinearLayout information_content = (LinearLayout) ll_view_more_information.findViewById(R.id.ll_information_content);
1324
                if (!view_all_information) {
1325
                    view_all_information = true;
1326
                    information_content.setVisibility(View.GONE);
1327
                    informationHeader.setCompoundDrawablesWithIntrinsicBounds(null, null, getActivity().getResources().getDrawable(R.drawable.ic_expand_more_grey600), null);
1328
                } else {
1329
                    view_all_information = false;
1330
                    information_content.setVisibility(View.VISIBLE);
1331
                    informationHeader.setCompoundDrawablesWithIntrinsicBounds(null, null, getActivity().getResources().getDrawable(R.drawable.ic_expand_less_grey600), null);
1332
                }
1333
            }
1334
        };
1335

    
1336
        private final View.OnClickListener expander_permissions = new View.OnClickListener() {
1337
            public void onClick(View v) {
1338
                final TextView permissionHeader = (TextView) ll_view_more_permissions.findViewById(R.id.permissions);
1339
                final TextView permissionListView = (TextView) ll_view_more_permissions.findViewById(R.id.permissions_list);
1340
                if (!view_all_permissions) {
1341
                    view_all_permissions = true;
1342
                    permissionListView.setVisibility(View.GONE);
1343
                    permissionHeader.setCompoundDrawablesWithIntrinsicBounds(null, null, getActivity().getResources().getDrawable(R.drawable.ic_expand_more_grey600), null);
1344
                } else {
1345
                    view_all_permissions = false;
1346
                    CommaSeparatedList permsList = getApks().getItem(0).permissions;
1347
                    if (permsList == null) {
1348
                        permissionListView.setText(R.string.no_permissions);
1349
                    } else {
1350
                        Iterator<String> permissions = permsList.iterator();
1351
                        StringBuilder sb = new StringBuilder();
1352
                        while (permissions.hasNext()) {
1353
                            final String permissionName = permissions.next();
1354
                            try {
1355
                                final Permission permission = new Permission(getActivity(), permissionName);
1356
                                // TODO: Make this list RTL friendly
1357
                                sb.append("\t").append(permission.getName()).append('\n');
1358
                            } catch (PackageManager.NameNotFoundException e) {
1359
                                Log.e(TAG, "Permission not yet available: " + permissionName);
1360
                            }
1361
                        }
1362
                        if (sb.length() > 0) {
1363
                            sb.setLength(sb.length() - 1);
1364
                        }
1365
                        permissionListView.setText(sb.toString());
1366
                    }
1367
                    permissionListView.setVisibility(View.VISIBLE);
1368
                    permissionHeader.setCompoundDrawablesWithIntrinsicBounds(null, null, getActivity().getResources().getDrawable(R.drawable.ic_expand_less_grey600), null);
1369
                }
1370
            }
1371
        };
1372

    
1373
        private String descAntiFeature(String af) {
1374
            switch (af) {
1375
            case "Ads":
1376
                return getString(R.string.antiadslist);
1377
            case "Tracking":
1378
                return getString(R.string.antitracklist);
1379
            case "NonFreeNet":
1380
                return getString(R.string.antinonfreenetlist);
1381
            case "NonFreeAdd":
1382
                return getString(R.string.antinonfreeadlist);
1383
            case "NonFreeDep":
1384
                return getString(R.string.antinonfreedeplist);
1385
            case "UpstreamNonFree":
1386
                return getString(R.string.antiupstreamnonfreelist);
1387
            }
1388
            return null;
1389
        }
1390

    
1391
        public void updateViews(View view) {
1392

    
1393
            if (view == null) {
1394
                Log.e(TAG, "AppDetailsSummaryFragment.updateViews(): view == null. Oops.");
1395
                return;
1396
            }
1397

    
1398
            TextView signatureView = (TextView) view.findViewById(R.id.signature);
1399
            if (prefs.expertMode() && getInstalledSignature() != null) {
1400
                signatureView.setVisibility(View.VISIBLE);
1401
                signatureView.setText("Signed: " + getInstalledSignatureId());
1402
            } else {
1403
                signatureView.setVisibility(View.GONE);
1404
            }
1405

    
1406
        }
1407
    }
1408

    
1409
    public static class AppDetailsHeaderFragment extends Fragment implements View.OnClickListener {
1410

    
1411
        private AppDetailsData data;
1412
        private Button btMain;
1413
        private ProgressBar progressBar;
1414
        private TextView progressSize;
1415
        private TextView progressPercent;
1416
        private ImageButton cancelButton;
1417
        protected final DisplayImageOptions displayImageOptions;
1418
        public static boolean installed = false;
1419
        public static boolean updateWanted = false;
1420

    
1421
        public AppDetailsHeaderFragment() {
1422
            displayImageOptions = new DisplayImageOptions.Builder()
1423
                .cacheInMemory(true)
1424
                .cacheOnDisk(true)
1425
                .imageScaleType(ImageScaleType.NONE)
1426
                .showImageOnLoading(R.drawable.ic_repo_app_default)
1427
                .showImageForEmptyUri(R.drawable.ic_repo_app_default)
1428
                .bitmapConfig(Bitmap.Config.RGB_565)
1429
                .build();
1430
        }
1431

    
1432
        private App getApp() { return data.getApp(); }
1433

    
1434
        private ApkListAdapter getApks() { return data.getApks(); }
1435

    
1436
        @Override
1437
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
1438
            View view = inflater.inflate(R.layout.app_details_header, container, false);
1439
            setupView(view);
1440
            return view;
1441
        }
1442

    
1443
        @Override
1444
        public void onAttach(Activity activity) {
1445
            super.onAttach(activity);
1446
            data = (AppDetailsData)activity;
1447
        }
1448

    
1449
        private void setupView(View view) {
1450

    
1451
            // Set the icon...
1452
            ImageView iv = (ImageView) view.findViewById(R.id.icon);
1453
            ImageLoader.getInstance().displayImage(getApp().iconUrlLarge, iv,
1454
                    displayImageOptions);
1455

    
1456
            // Set the title
1457
            TextView tv = (TextView) view.findViewById(R.id.title);
1458
            tv.setText(getApp().name);
1459

    
1460
            btMain   = (Button) view.findViewById(R.id.btn_main);
1461
            progressBar     = (ProgressBar) view.findViewById(R.id.progress_bar);
1462
            progressSize    = (TextView) view.findViewById(R.id.progress_size);
1463
            progressPercent = (TextView) view.findViewById(R.id.progress_percentage);
1464
            cancelButton    = (ImageButton) view.findViewById(R.id.cancel);
1465
            progressBar.setIndeterminate(false);
1466
            cancelButton.setOnClickListener(this);
1467

    
1468
            updateViews(view);
1469
        }
1470

    
1471
        @Override
1472
        public void onResume() {
1473
            super.onResume();
1474
            updateViews();
1475
        }
1476

    
1477
        /**
1478
         * Displays empty, indeterminate progress bar and related views.
1479
         */
1480
        public void startProgress() {
1481
            setProgressVisible(true);
1482
            progressBar.setIndeterminate(true);
1483
            progressSize.setText("");
1484
            progressPercent.setText("");
1485
            updateViews();
1486
        }
1487

    
1488
        /**
1489
         * Updates progress bar and captions to new values (in bytes).
1490
         */
1491
        public void updateProgress(long progress, long total) {
1492
            long percent = progress * 100 / total;
1493
            setProgressVisible(true);
1494
            progressBar.setIndeterminate(false);
1495
            progressBar.setProgress((int) percent);
1496
            progressBar.setMax(100);
1497
            progressSize.setText(readableFileSize(progress) + " / " + readableFileSize(total));
1498
            progressPercent.setText(Long.toString(percent) + " %");
1499
        }
1500

    
1501
        /**
1502
         * Converts a number of bytes to a human readable file size (eg 3.5 GiB).
1503
         *
1504
         * Based on http://stackoverflow.com/a/5599842
1505
         */
1506
        public String readableFileSize(long bytes) {
1507
            final String[] units = getResources().getStringArray(R.array.file_size_units);
1508
            if (bytes <= 0) return "0 " + units[0];
1509
            int digitGroups = (int) (Math.log10(bytes) / Math.log10(1024));
1510
            return new DecimalFormat("#,##0.#")
1511
                    .format(bytes / Math.pow(1024, digitGroups)) + " " + units[digitGroups];
1512
        }
1513

    
1514
        /**
1515
         * Shows or hides progress bar and related views.
1516
         */
1517
        private void setProgressVisible(boolean visible) {
1518
            int state = (visible) ? View.VISIBLE : View.GONE;
1519
            progressBar.setVisibility(state);
1520
            progressSize.setVisibility(state);
1521
            progressPercent.setVisibility(state);
1522
            cancelButton.setVisibility(state);
1523
        }
1524

    
1525
        /**
1526
         * Removes progress bar and related views, invokes {@link #updateViews()}.
1527
         */
1528
        public void removeProgress() {
1529
            setProgressVisible(false);
1530
            updateViews();
1531
        }
1532

    
1533
        /**
1534
         * Cancels download and hides progress bar.
1535
         */
1536
        @Override
1537
        public void onClick(View view) {
1538
            AppDetails activity = (AppDetails) getActivity();
1539
            if (activity == null || activity.downloadHandler == null)
1540
                return;
1541

    
1542
            activity.downloadHandler.cancel(true);
1543
            activity.cleanUpFinishedDownload();
1544
            setProgressVisible(false);
1545
            updateViews();
1546
        }
1547

    
1548
        public void updateViews() {
1549
            updateViews(getView());
1550
        }
1551

    
1552
        public void updateViews(View view) {
1553
            TextView statusView = (TextView) view.findViewById(R.id.status);
1554
            btMain.setVisibility(View.VISIBLE);
1555

    
1556
            AppDetails activity = (AppDetails) getActivity();
1557
            if (activity.downloadHandler != null) {
1558
                btMain.setText(R.string.downloading);
1559
                btMain.setEnabled(false);
1560
            // Check count > 0 due to incompatible apps resulting in an empty list.
1561
            // If App isn't installed
1562
            } else if (!getApp().isInstalled() && getApp().suggestedVercode > 0 &&
1563
                    ((AppDetails)getActivity()).adapter.getCount() > 0) {
1564
                installed = false;
1565
                statusView.setText(R.string.details_notinstalled);
1566
                NfcHelper.disableAndroidBeam(getActivity());
1567
                // Set Install button and hide second button
1568
                btMain.setText(R.string.menu_install);
1569
                btMain.setOnClickListener(mOnClickListener);
1570
                btMain.setEnabled(true);
1571
            // If App is installed
1572
            } else if (getApp().isInstalled()) {
1573
                installed = true;
1574
                statusView.setText(getString(R.string.details_installed, getApp().installedVersionName));
1575
                NfcHelper.setAndroidBeam(getActivity(), getApp().id);
1576
                if (getApp().canAndWantToUpdate()) {
1577
                    updateWanted = true;
1578
                    btMain.setText(R.string.menu_upgrade);
1579
                } else {
1580
                    updateWanted = false;
1581
                    if (((AppDetails)getActivity()).mPm.getLaunchIntentForPackage(getApp().id) != null){
1582
                        btMain.setText(R.string.menu_launch);
1583
                    } else {
1584
                        btMain.setText(R.string.menu_uninstall);
1585
                        if (!getApp().uninstallable) {
1586
                            btMain.setVisibility(View.GONE);
1587
                        }
1588
                    }
1589
                }
1590
                btMain.setOnClickListener(mOnClickListener);
1591
                btMain.setEnabled(true);
1592
            }
1593
            TextView currentVersion = (TextView) view.findViewById(R.id.current_version);
1594
            if (!getApks().isEmpty()) {
1595
                currentVersion.setText(getApks().getItem(0).version);
1596
            } else {
1597
                currentVersion.setVisibility(View.GONE);
1598
                btMain.setVisibility(View.GONE);
1599
            }
1600

    
1601
        }
1602

    
1603
        private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
1604
            public void onClick(View v) {
1605
                if (updateWanted) {
1606
                    if (getApp().suggestedVercode > 0) {
1607
                        final Apk apkToInstall = ApkProvider.Helper.find(getActivity(), getApp().id, getApp().suggestedVercode);
1608
                        ((AppDetails)getActivity()).install(apkToInstall);
1609
                        return;
1610
                    }
1611
                }
1612
                // If installed
1613
                if (installed) {
1614
                    // If "launchable", launch
1615
                    if (((AppDetails)getActivity()).mPm.getLaunchIntentForPackage(getApp().id) != null) {
1616
                        ((AppDetails)getActivity()).launchApk(getApp().id);
1617
                    } else {
1618
                        ((AppDetails)getActivity()).removeApk(getApp().id);
1619
                    }
1620
                // If not installed, install
1621
                } else if (getApp().suggestedVercode > 0) {
1622
                    btMain.setEnabled(false);
1623
                    btMain.setText(R.string.system_install_installing);
1624
                    final Apk apkToInstall = ApkProvider.Helper.find(getActivity(), getApp().id, getApp().suggestedVercode);
1625
                    ((AppDetails)getActivity()).install(apkToInstall);
1626
                }
1627
            }
1628
        };
1629
    }
1630

    
1631
    public static class AppDetailsListFragment extends ListFragment {
1632

    
1633
        private final String SUMMARY_TAG = "summary";
1634

    
1635
        private AppDetailsData data;
1636
        private AppInstallListener installListener;
1637
        private AppDetailsSummaryFragment summaryFragment = null;
1638

    
1639
        private FrameLayout headerView;
1640

    
1641
        @Override
1642
        public void onAttach(Activity activity) {
1643
            super.onAttach(activity);
1644
            data = (AppDetailsData)activity;
1645
            installListener = (AppInstallListener)activity;
1646
        }
1647

    
1648
        protected void install(final Apk apk) {
1649
            installListener.install(apk);
1650
        }
1651

    
1652
        protected void remove() {
1653
            installListener.removeApk(getApp().id);
1654
        }
1655

    
1656
        protected App getApp() { return data.getApp(); }
1657

    
1658
        protected ApkListAdapter getApks() { return data.getApks(); }
1659

    
1660
        @Override
1661
        public void onViewCreated(View view, Bundle savedInstanceState) {
1662
            // A bit of a hack, but we can't add the header view in setupSummaryHeader(),
1663
            // due to the fact it needs to happen before setListAdapter(). Also, seeing
1664
            // as we may never add a summary header (i.e. in landscape), this is probably
1665
            // the last opportunity to set the list adapter. As such, we use the headerView
1666
            // as a mechanism to optionally allow adding a header in the future.
1667
            if (headerView == null) {
1668
                headerView = new FrameLayout(getActivity());
1669
                headerView.setId(R.id.appDetailsSummaryHeader);
1670
            } else {
1671
                Fragment summaryFragment = getChildFragmentManager().findFragmentByTag(SUMMARY_TAG);
1672
                if (summaryFragment != null) {
1673
                    getChildFragmentManager().beginTransaction().remove(summaryFragment).commit();
1674
                }
1675
            }
1676

    
1677
            setListAdapter(null);
1678
            getListView().addHeaderView(headerView);
1679
            setListAdapter(getApks());
1680
        }
1681

    
1682
        @Override
1683
        public void onListItemClick(ListView l, View v, int position, long id) {
1684
            final Apk apk = getApks().getItem(position - l.getHeaderViewsCount());
1685
            if (getApp().installedVersionCode == apk.vercode) {
1686
                remove();
1687
            } else if (getApp().installedVersionCode > apk.vercode) {
1688
                AlertDialog.Builder ask_alrt = new AlertDialog.Builder(getActivity());
1689
                ask_alrt.setMessage(R.string.installDowngrade);
1690
                ask_alrt.setPositiveButton(R.string.yes,
1691
                        new DialogInterface.OnClickListener() {
1692
                            @Override
1693
                            public void onClick(DialogInterface dialog,
1694
                                    int whichButton) {
1695
                                install(apk);
1696
                            }
1697
                        });
1698
                ask_alrt.setNegativeButton(R.string.no,
1699
                        new DialogInterface.OnClickListener() {
1700
                            @Override
1701
                            public void onClick(DialogInterface dialog,
1702
                                    int whichButton) {
1703
                            }
1704
                        });
1705
                AlertDialog alert = ask_alrt.create();
1706
                alert.show();
1707
            } else {
1708
                install(apk);
1709
            }
1710
        }
1711

    
1712
        public void removeSummaryHeader() {
1713
            Fragment summary = getChildFragmentManager().findFragmentByTag(SUMMARY_TAG);
1714
            if (summary != null) {
1715
                getChildFragmentManager().beginTransaction().remove(summary).commit();
1716
                headerView.removeAllViews();
1717
                headerView.setVisibility(View.GONE);
1718
                summaryFragment = null;
1719
            }
1720
        }
1721

    
1722
        public void setupSummaryHeader() {
1723
            Fragment fragment = getChildFragmentManager().findFragmentByTag(SUMMARY_TAG);
1724
            if (fragment != null) {
1725
                summaryFragment = (AppDetailsSummaryFragment)fragment;
1726
            } else {
1727
                summaryFragment = new AppDetailsSummaryFragment();
1728
            }
1729
            getChildFragmentManager().beginTransaction().replace(headerView.getId(), summaryFragment, SUMMARY_TAG).commit();
1730
            headerView.setVisibility(View.VISIBLE);
1731
        }
1732
    }
1733

    
1734
}