Statistics
| Branch: | Tag: | Revision:

storymaker / app / src / org / storymaker / app / BaseHomeActivity.java @ 5debefcb

History | View | Annotate | Download (42 KB)

1
package org.storymaker.app;
2

    
3
import android.Manifest;
4
import android.app.AlertDialog;
5
import android.app.ProgressDialog;
6
import android.content.ComponentName;
7
import android.content.Context;
8
import android.content.DialogInterface;
9
import android.content.Intent;
10
import android.content.SharedPreferences;
11
import android.content.pm.PackageManager;
12
import android.net.Uri;
13
import android.os.AsyncTask;
14
import android.os.Bundle;
15
import android.preference.PreferenceManager;
16
import android.support.v4.app.ActivityCompat;
17
import android.support.v4.view.ViewPager;
18
import android.support.v7.widget.RecyclerView;
19
import android.util.Log;
20
import android.view.Menu;
21
import android.view.MenuItem;
22
import android.widget.Toast;
23

    
24
import com.hannesdorfmann.sqlbrite.dao.DaoManager;
25

    
26
import net.hockeyapp.android.CrashManager;
27
import net.hockeyapp.android.CrashManagerListener;
28
import net.hockeyapp.android.UpdateManager;
29
import net.sqlcipher.Cursor;
30
import net.sqlcipher.database.SQLiteDatabase;
31
import net.sqlcipher.database.SQLiteDatabaseHook;
32

    
33
import org.apache.commons.io.FileUtils;
34
import org.apache.commons.io.FilenameUtils;
35
import org.apache.commons.io.filefilter.WildcardFileFilter;
36
import org.apache.commons.lang3.StringUtils;
37
import org.storymaker.app.model.Project;
38
import org.storymaker.app.server.LoginActivity;
39
import org.storymaker.app.server.ServerManager;
40
import org.storymaker.app.ui.SlidingTabLayout;
41

    
42
import java.io.BufferedReader;
43
import java.io.File;
44
import java.io.IOException;
45
import java.io.InputStreamReader;
46
import java.util.ArrayList;
47
import java.util.HashMap;
48
import java.util.List;
49

    
50
import info.guardianproject.netcipher.proxy.OrbotHelper;
51
import rx.functions.Action1;
52
import scal.io.liger.JsonHelper;
53
import scal.io.liger.MainActivity;
54
import scal.io.liger.StorageHelper;
55
import scal.io.liger.StorymakerIndexManager;
56
import scal.io.liger.model.ContentPackMetadata;
57
import scal.io.liger.model.StoryPathLibrary;
58
import scal.io.liger.model.sqlbrite.AvailableIndexItem;
59
import scal.io.liger.model.sqlbrite.AvailableIndexItemDao;
60
import scal.io.liger.model.sqlbrite.ExpansionIndexItem;
61
import scal.io.liger.model.sqlbrite.InstalledIndexItem;
62
import scal.io.liger.model.sqlbrite.InstalledIndexItemDao;
63
import scal.io.liger.model.sqlbrite.InstanceIndexItem;
64
import scal.io.liger.model.sqlbrite.InstanceIndexItemDao;
65
import scal.io.liger.model.sqlbrite.QueueItemDao;
66
import timber.log.Timber;
67

    
68
/**
69
 * Created by josh on 12/18/15.
70
 */
71
public abstract class BaseHomeActivity extends BaseActivity {
72

    
73
    protected ProgressDialog mLoading;
74
    protected ArrayList<Project> mListProjects;
75
    protected RecyclerView mRecyclerView;
76

    
77
    protected ViewPager mViewPager;
78
    protected SlidingTabLayout mSlidingTabLayout;
79
    protected String[] mTabMenu;
80

    
81
    protected boolean loggedIn;
82

    
83
    // new stuff
84
    protected InstanceIndexItemDao instanceIndexItemDao;
85
    protected AvailableIndexItemDao availableIndexItemDao;
86
    protected InstalledIndexItemDao installedIndexItemDao;
87
    protected QueueItemDao queueItemDao;
88
    protected DaoManager daoManager;
89
    protected int dbVersion = 2;
90

    
91
    protected HashMap<String, ArrayList<Thread>> downloadThreads = new HashMap<String, ArrayList<Thread>>();
92

    
93
    public void removeThreads(String id) {
94
        if (downloadThreads.containsKey(id)) {
95
            downloadThreads.remove(id);
96
        }
97
    }
98

    
99
    public BaseHomeActivity() {
100

    
101
        instanceIndexItemDao = new InstanceIndexItemDao();
102
        availableIndexItemDao = new AvailableIndexItemDao();
103
        installedIndexItemDao = new InstalledIndexItemDao();
104
        queueItemDao = new QueueItemDao();
105

    
106
        daoManager = new DaoManager(BaseHomeActivity.this, "Storymaker.db", dbVersion, instanceIndexItemDao, availableIndexItemDao, installedIndexItemDao, queueItemDao);
107
        daoManager.setLogging(false);
108

    
109
        //Log.d("BaseHomeActivity", "DB version "+daoManager.getVersion());
110

    
111

    
112
    }
113

    
114
    public String[] getMenu(String menu_type) {
115

    
116
        //This Method Transposes between an arrays.xml array of ids to build the catalog menu
117
        //      and their strings.xml display name counterparts
118
        //      the idea is the ids can be consistent (they will be part of database queries),
119
        //      whereas the Menu tab names can be localized
120
        //
121
        //      ex: in arrays.xml
122
        //
123
        //              <string-array name="catalog_menu_ids">
124
        //                  <item>catalog</item>
125
        //                  <item>guides</item>
126
        //                  <item>lessons</item>
127
        //                  <item>templates</item>
128
        //              </string-array>
129
        //
130
        //      ex: in strings.xml
131
        //
132
        //              <string name="catalog_menu_catalog">Catalog</string>
133
        //              <string name="catalog_menu_guides">Guides</string>
134
        //              <string name="catalog_menu_lessons">Lessons</string>
135
        //              <string name="catalog_menu_templates">Templates</string>
136
        //
137

    
138
        int menuId = getResources().getIdentifier(menu_type + "_menu_ids", "array", getApplicationContext().getPackageName());
139
        String[] menu_ids = getResources().getStringArray(menuId);
140
        String[] menu_names = new String[menu_ids.length];
141

    
142
        for (int i=0; i<menu_ids.length; i++) {
143
            int id = getResources().getIdentifier("home_menu_" + menu_ids[i], "string", getApplicationContext().getPackageName());
144
            String menu_name = getResources().getString(id);
145
            menu_names[i] = menu_name;
146
        }
147

    
148
        //String s = getString(R.string.finished_downloading);
149

    
150
        return menu_names;
151

    
152
    }
153

    
154
    // added for testing
155
    public void scroll(int position) {
156
        Timber.d("Scrolling to index item " + position);
157
        mRecyclerView.scrollToPosition(position);
158
    }
159

    
160
    public static String parseInstanceDate(String filename) {
161
//        String jsonFilePath = storyPath.buildTargetPath(storyPath.getId() + "-instance-" + timeStamp.getTime() + ".json");
162
        String[] splits = FilenameUtils.removeExtension(filename).split("-");
163
        return splits[splits.length - 1]; // FIXME make more robust and move into liger
164
    }
165

    
166
    // copied this as a short term fix until we get loading cleanly split out from the liger sample app ui stuff
167
    protected StoryPathLibrary initSPLFromJson(String json, String jsonPath) {
168
        if (json == null || json.equals("")) { // FIXME use StringUtils
169
            Toast.makeText(this, getString(R.string.home_content_missing), Toast.LENGTH_LONG).show();
170
            finish();
171
            return null;
172
        }
173

    
174
        ArrayList<String> referencedFiles = null;
175

    
176
        // should not need to insert dependencies into a saved instance
177
        if (jsonPath.contains("instance")) {
178
            referencedFiles = new ArrayList<String>();
179
        } else {
180
            referencedFiles = JsonHelper.getInstancePaths(this);
181
        }
182

    
183
        StoryPathLibrary storyPathLibrary = JsonHelper.deserializeStoryPathLibrary(json, jsonPath, referencedFiles, this, StoryMakerApp.getCurrentLocale().getLanguage());
184

    
185
        if ((storyPathLibrary != null) && (storyPathLibrary.getCurrentStoryPathFile() != null)) {
186
            storyPathLibrary.loadStoryPathTemplate("CURRENT", false);
187
        }
188

    
189
        return storyPathLibrary;
190
    }
191

    
192
    @Override
193
    public void onCreate(Bundle savedInstanceState) {
194
        super.onCreate(savedInstanceState);
195

    
196
        // copy index file
197
        StorymakerIndexManager.copyAvailableIndex(this, false); // TODO: REPLACE THIS WITH INDEX DOWNLOAD (IF LOGGED IN) <- NEED TO COPY FILE FOR BASELINE CONTENT
198

    
199
        // initialize db
200

    
201
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
202

    
203
        // version check (sqlite upgrade requires migration)
204

    
205
        int appMigrationVersion = preferences.getInt("APP_MIGRATION_VERSION", 0);
206

    
207
        Timber.d("MIGRATION CHECK: " + appMigrationVersion + " vs. " + Constants.APP_MIGRATION_VERSION);
208

    
209
        if (appMigrationVersion != Constants.APP_MIGRATION_VERSION) {
210

    
211
            Timber.d("MIGRATION REQUIRED, RE-ENCRYPTING DATABASE");
212

    
213
            final boolean[] dbStatus = {false};
214
            try {
215
                SQLiteDatabaseHook dbHook = new SQLiteDatabaseHook() {
216
                    public void preKey(SQLiteDatabase database) {
217
                    }
218
                    public void postKey(SQLiteDatabase database) {
219
                        Cursor cursor = database.rawQuery("PRAGMA cipher_migrate", new String[]{});
220
                        String value = "";
221
                        if (cursor != null) {
222
                            cursor.moveToFirst();
223
                            value = cursor.getString(0);
224
                            cursor.close();
225
                        }
226

    
227
                        // this result is currently ignored, checking if db is null instead
228
                        dbStatus[0] = Integer.valueOf(value) == 0;
229
                    }
230
                };
231

    
232
                File dbPath = getDatabasePath("sm.db");
233
                Timber.d("MIGRATING DATABASE AT " + dbPath.getPath());
234

    
235
                SQLiteDatabase sqldb = SQLiteDatabase.openOrCreateDatabase(dbPath, "foo", null, dbHook);
236
                if (sqldb != null) {
237
                    Timber.d("MIGRATED DATABASE NOT NULL");
238
                    sqldb.close();
239

    
240
                    // update preferences if migration succeeded
241

    
242
                    preferences.edit().putInt("APP_MIGRATION_VERSION", Constants.APP_MIGRATION_VERSION).commit();
243
                } else {
244
                    Timber.e("MIGRATED DATABASE IS NULL");
245
                }
246
            } catch (Exception ex) {
247
                Timber.e("EXCEPTION WHILE MIGRATING DATABASE: " + ex.getMessage());
248
            }
249
        }
250

    
251
        int availableIndexVersion = preferences.getInt("AVAILABLE_INDEX_VERSION", 0);
252

    
253
        Timber.d("VERSION CHECK: " + availableIndexVersion + " vs. " + scal.io.liger.Constants.AVAILABLE_INDEX_VERSION);
254

    
255
        if (availableIndexVersion != scal.io.liger.Constants.AVAILABLE_INDEX_VERSION) {
256

    
257
            // load db from file
258

    
259
            HashMap<String, scal.io.liger.model.ExpansionIndexItem> availableItemsFromFile = scal.io.liger.IndexManager.loadAvailableIdIndex(this);
260

    
261
            if (availableItemsFromFile.size() == 0) {
262
                Timber.d("NOTHING LOADED FROM AVAILABLE FILE");
263
            } else {
264
                for (scal.io.liger.model.ExpansionIndexItem item : availableItemsFromFile.values()) {
265
                    Timber.d("ADDING " + item.getExpansionId() + " TO DATABASE (AVAILABLE)");
266
                    availableIndexItemDao.addAvailableIndexItem(item, true); // replaces existing items, should trigger updates to installed items and table as needed
267

    
268
                    // ugly solution to deal with the fact that the popup menu assumes there will be threads for an item we tried to download/install
269
                    ArrayList<Thread> noThreads = new ArrayList<Thread>();
270
                    downloadThreads.put(item.getExpansionId(), noThreads);
271

    
272
                }
273
            }
274

    
275
            // the following migration stuff is currently piggy-backing on the index update stuff
276

    
277
            // if found, migrate installed index
278

    
279
            File installedFile = new File(StorageHelper.getActualStorageDirectory(this), "installed_index.json");
280

    
281
            if (installedFile.exists()) {
282
                HashMap<String, scal.io.liger.model.ExpansionIndexItem> installedItemsFromFile = scal.io.liger.IndexManager.loadInstalledIdIndex(this);
283

    
284
                if (installedItemsFromFile.size() == 0) {
285
                    Timber.d("NOTHING LOADED FROM INSTALLED INDEX FILE");
286
                } else {
287
                    for (scal.io.liger.model.ExpansionIndexItem item : installedItemsFromFile.values()) {
288
                        Timber.d("ADDING " + item.getExpansionId() + " TO DATABASE (INSTALLED)");
289
                        installedIndexItemDao.addInstalledIndexItem(item, true); // replaces existing items, should trigger updates to installed items and table as needed
290
                    }
291
                }
292

    
293
                installedFile.delete();
294
            } else {
295
                Timber.d("NO INSTALLED INDEX FILE");
296
            }
297

    
298
            // if found, migrate instance index
299

    
300
            File instanceFile = new File(StorageHelper.getActualStorageDirectory(this), "instance_index.json");
301

    
302
            if (instanceFile.exists()) {
303
                HashMap<String, scal.io.liger.model.InstanceIndexItem> instanceItemsFromFile = scal.io.liger.IndexManager.loadInstanceIndex(this);
304

    
305
                if (instanceItemsFromFile.size() == 0) {
306
                    Timber.d("NOTHING LOADED FROM INSTANCE INDEX FILE");
307
                } else {
308
                    for (scal.io.liger.model.InstanceIndexItem item : instanceItemsFromFile.values()) {
309
                        Timber.d("ADDING " + item.getInstanceFilePath() + " TO DATABASE (INSTANCE)");
310
                        instanceIndexItemDao.addInstanceIndexItem(item, true); // replaces existing items, should trigger updates to installed items and table as needed
311
                    }
312
                }
313

    
314
                instanceFile.delete();
315
            } else {
316
                Timber.d("NO INSTANCE INDEX FILE");
317
            }
318

    
319
            // update preferences
320

    
321
            preferences.edit().putInt("AVAILABLE_INDEX_VERSION", scal.io.liger.Constants.AVAILABLE_INDEX_VERSION).commit();
322

    
323
            if (getIntent() != null && getIntent().hasExtra("showlauncher")) {
324
                if (getIntent().getBooleanExtra("showlauncher", false)) {
325
                    showLauncherIcon();
326
                }
327
            }
328
        }
329

    
330

    
331

    
332
        // dumb test
333

    
334
        // check values
335
        availableIndexItemDao.getAvailableIndexItems().take(1).subscribe(new Action1<List<AvailableIndexItem>>() {
336

    
337
            @Override
338
            public void call(List<AvailableIndexItem> expansionIndexItems) {
339

    
340
                // just process the list
341

    
342
                for (ExpansionIndexItem item : expansionIndexItems) {
343
                    Timber.d("AVAILABLE ITEM " + item.getExpansionId() + ", TITLE: " + item.getTitle());
344
                }
345
            }
346
        });
347

    
348
        installedIndexItemDao.getInstalledIndexItems().take(1).subscribe(new Action1<List<InstalledIndexItem>>() {
349

    
350
            @Override
351
            public void call(List<InstalledIndexItem> expansionIndexItems) {
352

    
353
                // just process the list
354

    
355
                for (ExpansionIndexItem item : expansionIndexItems) {
356
                    Timber.d("INSTALLED ITEM " + item.getExpansionId() + ", TITLE: " + item.getTitle());
357
                }
358
            }
359
        });
360

    
361

    
362

    
363
        // file cleanup
364
        File actualStorageDirectory = StorageHelper.getActualStorageDirectory(this);
365

    
366
        if (actualStorageDirectory != null) {
367
            JsonHelper.cleanup(actualStorageDirectory.getPath());
368
        } else {
369
            // this is an error, will deal with it below
370
        }
371

    
372
        // default
373
        loggedIn = false;
374

    
375
        // set title bar as a reminder if test server is specified
376
        //getActionBar().setTitle(Utils.getAppName(this));
377

    
378
        if (actualStorageDirectory != null) {
379
            // NEW/TEMP
380
            // DOWNLOAD AVAILABE INDEX FOR CURRENT USER AND SAVE TO TARGET FILE
381
            // NEED TO ACCOUNT FOR POSSIBLE MISSING INDEX
382
            IndexTask iTask = new IndexTask(this, true); // force download at startup (maybe only force on a timetable?)
383
            iTask.execute();
384
        } else {
385
            //show storage error message
386
            new AlertDialog.Builder(this)
387
                    .setTitle(Utils.getAppName(this))
388
                    .setIcon(android.R.drawable.ic_dialog_info)
389
                    .setMessage(R.string.err_storage_not_available)
390
                    .show();
391
        }
392

    
393
        // we want to grab required updates without restarting the app
394
        // integrate with index task
395
        // if (!DownloadHelper.checkAndDownload(this)) {
396
        //     Toast.makeText(this, "Downloading content and/or updating installed files", Toast.LENGTH_LONG).show(); // FIXME move to strings.xml
397
        // }
398

    
399
        // i don't think we ever want to do this
400
        // IndexManager.copyInstalledIndex(this);
401

    
402
        //setContentView(R.layout.activity_home);
403

    
404
        //mRecyclerView = (RecyclerView) findViewById(scal.io.liger.R.id.recyclerView);
405
        //mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
406

    
407
        //mSwipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipeRefreshLayout);
408
        //mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
409
        //    @Override
410
        //    public void onRefresh() {
411
        //        IndexTask iTask = new IndexTask(HomeActivity.this, true); // force download on manual refresh
412
        //        iTask.execute();
413
        //    }
414
        //});
415

    
416

    
417
        //mTabMenu = getMenu("home");
418
        // action bar stuff
419
        getActionBar().setDisplayHomeAsUpEnabled(true);
420

    
421
        checkForTor();
422

    
423
        checkForUpdates();
424

    
425
    }
426
    
427

    
428
    public static void launchLiger(Context context, String splId, String instancePath, String splPath) {
429

    
430
        // TEMP - do we need to check files for anything besides the default library?
431
        /*
432
        if (!DownloadHelper.checkAllFiles(context)) { // FIXME the app should define these, not the library
433
            Toast.makeText(context, "Please wait for the content pack to finish downloading", Toast.LENGTH_LONG).show(); // FIXME move to strings.xml
434
            return;
435
        }
436
        */
437

    
438
        if ((splId != null) && (splId.equals("default_library"))) { // FIXME use StringUtils
439

    
440
            // initiate check/download for main/patch expansion files
441
            boolean readyToOpen = StorymakerDownloadHelper.checkAndDownloadNew(context);
442

    
443
            if (!readyToOpen) {
444
                // if file is being downloaded, don't open
445
                Timber.d("CURRENTLY DOWNLOADING FILE");
446

    
447
                Toast.makeText(context, context.getString(R.string.home_please_wait), Toast.LENGTH_LONG).show();
448
                return;
449
            }
450

    
451
        }
452

    
453
        Intent ligerIntent = new Intent(context, MainActivity.class);
454
        ligerIntent.putExtra(MainActivity.INTENT_KEY_WINDOW_TITLE, Utils.getAppName(context));
455
        String lang = StoryMakerApp.getCurrentLocale().getLanguage();
456
        ligerIntent.putExtra("lang", lang);
457

    
458
        SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
459
        int pslideduration = Integer.parseInt(settings.getString("pslideduration", "5"));
460
        ligerIntent.putExtra("photo_essay_slide_duration", pslideduration * 1000);
461
        if (splId != null && !splId.isEmpty()) {
462
            ligerIntent.putExtra(MainActivity.INTENT_KEY_STORYPATH_LIBRARY_ID, splId);
463
        } else if (splPath != null && !splPath.isEmpty()) {
464
            ligerIntent.putExtra(MainActivity.INTENT_KEY_STORYPATH_LIBRARY_PATH, splPath);
465
        } else if (instancePath != null && !instancePath.isEmpty()) {
466
            ligerIntent.putExtra(MainActivity.INTENT_KEY_STORYPATH_INSTANCE_PATH, instancePath);
467
        }
468
        context.startActivity(ligerIntent);
469
    }
470

    
471
    protected class IndexTask extends AsyncTask<Void, Void, Boolean> {
472

    
473
        // TODO: ADJUST THIS TO ACCOUNT FOR STORING AVAILABLE INDEX IN DB
474

    
475
        private Context mContext;
476
        private boolean forceDownload;
477

    
478
        public IndexTask(Context context, boolean forceDownload) {
479
            this.mContext = context;
480
            this.forceDownload = forceDownload;
481
        }
482

    
483
        @Override
484
        protected Boolean doInBackground(Void... params) {
485

    
486
            Timber.d("IndexTask.doInBackground IS RUNNING");
487

    
488
            boolean loginRequest = false;
489

    
490
            ServerManager sm = StoryMakerApp.getServerManager();
491

    
492
            if (mCacheWordHandler.isLocked()) {
493

    
494
                // prevent credential check attempt if database is locked
495
                Timber.d("cacheword locked, skipping index credential check");
496
                // user is not logged in, update status flag if necessary
497
                if (loggedIn) {
498
                    loggedIn = false;
499
                }
500

    
501
            } else if (sm.hasCreds()) {
502
                // user is logged in, update status flag if necessary
503
                if (!loggedIn) {
504
                    loggedIn = true;
505
                    loginRequest = true; // user just logged in, need to check server
506
                }
507
            } else {
508
                // user is not logged in, update status flag if necessary
509
                if (loggedIn) {
510
                    loggedIn = false;
511
                }
512
            }
513

    
514
            // check server if user just logged in
515
            if (loginRequest) {
516
                Timber.d("USER LOGGED IN, CHECK SERVER");
517

    
518
                // reset available index
519
                StorymakerIndexManager.copyAvailableIndex(mContext, false);
520

    
521
                // attempt to download new assignments
522
                return Boolean.valueOf(sm.index());
523
            }
524

    
525
            // check server if user insists (if database is unlocked)
526
            if (forceDownload && !mCacheWordHandler.isLocked()) {
527
                Timber.d("UPDATE REQUIRED, CHECK SERVER");
528

    
529
                // reset available index
530
                StorymakerIndexManager.copyAvailableIndex(mContext, false);
531

    
532
                // attempt to download new assignments
533
                return Boolean.valueOf(sm.index());
534
            }
535

    
536
            // no-op
537
            return false;
538
        }
539

    
540
        protected void onPostExecute(Boolean result) {
541
            if (result.booleanValue()) {
542
                Timber.d("DOWNLOADED ASSIGNMENTS AND UPDATED AVAILABLE INDEX");
543
            } else {
544
                Timber.d("DID NOT DOWNLOAD ASSIGNMENTS OR UPDATE AVAILABLE INDEX");
545
            }
546

    
547
            //RES
548
            //mSwipeRefreshLayout.setRefreshing(false);
549

    
550
            // resolve available/installed conflicts and grab updates if needed
551
            if (!StorymakerDownloadHelper.checkAndDownload(mContext, availableIndexItemDao, installedIndexItemDao, queueItemDao)) {
552
                Toast.makeText(mContext, getString(R.string.home_downloading_content), Toast.LENGTH_LONG).show();
553
            }
554
            // refresh regardless (called from onResume and OnRefreshListener)
555
            initActivityList();
556
        }
557
    }
558

    
559
    @Override
560
    public void onSaveInstanceState(Bundle outState) {
561
        super.onSaveInstanceState(outState);
562
    }
563

    
564
    //if the user hasn't registered with the user, show the login screen
565
    private void checkCreds() {
566

    
567
        SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
568

    
569
        String user = settings.getString("user", null);
570

    
571
        if (user == null) {
572
            Intent intent = new Intent(this, LoginActivity.class);
573
            startActivity(intent);
574
        }
575
    }
576

    
577
    @Override
578
    public boolean onCreateOptionsMenu(Menu menu) {
579
        getMenuInflater().inflate(R.menu.activity_home, menu);
580
        return true;
581
    }
582

    
583

    
584
    public void launchNewProject() {
585
        // need to check this to determine whether there is a storage issue that will cause a crash
586
        File actualStorageDirectory = StorageHelper.getActualStorageDirectory(this);
587

    
588
        if (actualStorageDirectory != null) {
589
            launchLiger(this, "default_library", null, null);
590
        } else {
591
            //show storage error message
592
            new AlertDialog.Builder(this)
593
                    .setTitle(Utils.getAppName(this))
594
                    .setIcon(android.R.drawable.ic_dialog_info)
595
                    .setMessage(R.string.err_storage_not_available)
596
                    .show();
597
        }
598
    }
599

    
600
    @Override
601
    public boolean onOptionsItemSelected(MenuItem item) {
602

    
603
        if (item.getItemId() == android.R.id.home) {
604
            toggleDrawer();
605
            return true;
606
//        } else if (item.getItemId() == R.id.menu_new_project) {
607
//            // need to check this to determine whether there is a storage issue that will cause a crash
608
//            File actualStorageDirectory = StorageHelper.getActualStorageDirectory(this);
609
//
610
//            if (actualStorageDirectory != null) {
611
//                launchNewProject();
612
//            } else {
613
//                //show storage error message
614
//                new AlertDialog.Builder(this)
615
//                        .setTitle(Utils.getAppName(this))
616
//                        .setIcon(android.R.drawable.ic_dialog_info)
617
//                        .setMessage(R.string.err_storage_not_available)
618
//                        .show();
619
//            }
620
//
621
//            return true;
622
        } else if (item.getItemId() == R.id.menu_about) {
623
            String url = "https://storymaker.org";
624

    
625
            Intent i = new Intent(Intent.ACTION_VIEW);
626
            i.setData(Uri.parse(url));
627
            startActivity(i);
628
            return true;
629
        } else if (item.getItemId() == R.id.menu_hide) {
630
            hideLauncherIcon();
631
        }
632

    
633
        return super.onOptionsItemSelected(item);
634
    }
635

    
636
    protected abstract void initActivityList();
637

    
638
    protected void checkForUpdates() {
639
        if (BuildConfig.DEBUG) {
640
            UpdateManager.register(this, AppConstants.HOCKEY_APP_ID);
641
        }
642
    }
643

    
644
    protected void checkForTor() {
645
         SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
646

    
647
         boolean useTor = settings.getBoolean("pusetor", false);
648

    
649
         if (useTor) {
650

    
651
             if (!OrbotHelper.isOrbotInstalled(this)) {
652
                startActivity(OrbotHelper.getOrbotInstallIntent(this));
653
             } else if (!OrbotHelper.isOrbotRunning(this)) {
654
                OrbotHelper.requestStartTor(this);
655
             }
656
         }
657
    }
658

    
659
    protected static final ComponentName LAUNCHER_COMPONENT_NAME = new ComponentName(
660
            "org.storymaker.app", "org.storymaker.app.Launcher");
661

    
662
    protected void hideLauncherIcon() {
663

    
664
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
665
        builder.setTitle("Important!");
666
        builder.setMessage("This will hide the app's icon in the launcher.\n\nTo show the app again, dial phone number 98765.");
667
        builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
668
            public void onClick(DialogInterface dialog, int which) {
669

    
670
                getPackageManager().setComponentEnabledSetting(LAUNCHER_COMPONENT_NAME,
671
                        PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
672
                        PackageManager.DONT_KILL_APP);
673

    
674
                finish();
675
            }
676
        });
677
        builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
678
            public void onClick(DialogInterface dialog, int which) {
679

    
680

    
681
            }
682
        });
683
        builder.setCancelable(true);
684
        builder.setIcon(android.R.drawable.ic_dialog_alert);
685
        builder.show();
686
    }
687

    
688
    protected void showLauncherIcon() {
689
        getPackageManager().setComponentEnabledSetting(LAUNCHER_COMPONENT_NAME,
690
                PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
691
                PackageManager.DONT_KILL_APP);
692
    }
693

    
694
    protected boolean isLauncherIconVisible() {
695
        int enabledSetting = getPackageManager()
696
                .getComponentEnabledSetting(LAUNCHER_COMPONENT_NAME);
697
        return enabledSetting != PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
698
    }
699

    
700
    protected void checkForCrashes() {
701
        //CrashManager.register(this, AppConstants.HOCKEY_APP_ID);
702
        CrashManager.register(this, AppConstants.HOCKEY_APP_ID, new CrashManagerListener() {
703
            public String getDescription() {
704
                String description = "";
705

    
706
                try {
707
                    //Process process = Runtime.getRuntime().exec("logcat -d HockeyApp:D *:S");
708
                    Process process = Runtime.getRuntime().exec("logcat -d");
709
                    BufferedReader bufferedReader =
710
                            new BufferedReader(new InputStreamReader(process.getInputStream()));
711

    
712
                    StringBuilder log = new StringBuilder();
713
                    String line;
714
                    while ((line = bufferedReader.readLine()) != null) {
715
                        log.append(line);
716
                        log.append(System.getProperty("line.separator"));
717
                    }
718
                    bufferedReader.close();
719

    
720
                    description = log.toString();
721
                } catch (IOException e) {
722
                }
723

    
724
                return description;
725
            }
726
        });
727
    }
728

    
729
    protected void showPreferences ()
730
    {
731
        Intent intent = new Intent(this,SimplePreferences.class);
732
        this.startActivityForResult(intent, 9999);
733
    }
734

    
735
    public class PauseListener implements DialogInterface.OnClickListener {
736

    
737
        private scal.io.liger.model.sqlbrite.ExpansionIndexItem eItem;
738

    
739
        public PauseListener(scal.io.liger.model.sqlbrite.ExpansionIndexItem eItem) {
740
            super();
741

    
742
            this.eItem = eItem;
743
        }
744

    
745
        @Override
746
        public void onClick(DialogInterface dialog, int which) {
747

    
748
            Timber.d("PAUSE...");
749

    
750
            // stop associated threads
751

    
752
            ArrayList<Thread> currentThreads = downloadThreads.get(eItem.getExpansionId());
753

    
754
            if (currentThreads != null) {
755
                for (Thread thread : currentThreads) {
756
                    Timber.d("STOPPING THREAD " + thread.getId());
757
                    thread.interrupt();
758
                }
759
            }
760

    
761
            downloadThreads.remove(eItem.getExpansionId());
762

    
763
        }
764
    }
765

    
766
    @Override
767
    public void onResume() {
768
        super.onResume();
769

    
770
        getActionBar().setTitle(Utils.getAppName(this));
771

    
772
        checkForCrashes();
773

    
774
        //if (!DownloadHelper.checkAllFiles(this) && downloadPoller == null) {
775
        // integrate with index task
776
        //if (!DownloadHelper.checkAndDownload(this)) {
777
        // don't poll, just pop up message if a download was initiated
778
        //downloadPoller = new DownloadPoller();
779
        //downloadPoller.execute("foo");
780
        //    Toast.makeText(this, "Downloading content and/or updating installed files", Toast.LENGTH_LONG).show(); // FIXME move to strings.xml
781
        //} //else {
782
        // merge this with index task
783
        //   initActivityList();
784

    
785
        // need to check this to determine whether there is a storage issue that will cause a crash
786
        File actualStorageDirectory = StorageHelper.getActualStorageDirectory(this);
787

    
788
        if (actualStorageDirectory != null) {
789
            IndexTask iTask = new IndexTask(this, false); // don't force download on resume (currently triggers only on login)
790
            iTask.execute();
791
        } else {
792
            //show storage error message
793
            new AlertDialog.Builder(this)
794
                    .setTitle(Utils.getAppName(this))
795
                    .setIcon(android.R.drawable.ic_dialog_info)
796
                    .setMessage(R.string.err_storage_not_available)
797
                    .show();
798
        }
799

    
800
        //}
801

    
802
        boolean isExternalStorageReady = Utils.Files.isExternalStorageReady();
803

    
804
        if (!isExternalStorageReady) {
805
            //show storage error message
806
            new AlertDialog.Builder(this)
807
                    .setTitle(Utils.getAppName(this))
808
                    .setIcon(android.R.drawable.ic_dialog_info)
809
                    .setMessage(R.string.err_storage_not_ready)
810
                    .show();
811

    
812
        }
813

    
814
        if (getIntent() != null && getIntent().hasExtra("showlauncher")) {
815
            if (getIntent().getBooleanExtra("showlauncher", false)) {
816
                showLauncherIcon();
817
            }
818
        }
819

    
820
        checkAndEnforcePermissions(); // FIXME we should handle these at time of use instead of like this
821
    }
822

    
823
    protected void showSPLSelectorPopup(final String[] names, final String[] paths) {
824
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
825

    
826
        builder.setTitle("Choose Story File(SdCard/Liger/)").setItems(names, new DialogInterface.OnClickListener() {
827
            public void onClick(DialogInterface dialog, int index) {
828
                launchLiger(BaseHomeActivity.this, null, null, paths[index]);
829
            }
830
        });
831

    
832
        AlertDialog alert = builder.create();
833
        alert.show();
834
    }
835

    
836
    // HAD TO SPLIT OUT INTO A METHOD
837
    public void handleClick(ExpansionIndexItem eItem, HashMap<String, ExpansionIndexItem> installedIds, boolean showDialog) {
838

    
839
        // initiate check/download whether installed or not
840
        HashMap<String, Thread> newThreads = StorymakerDownloadHelper.checkAndDownload(BaseHomeActivity.this, eItem, installedIndexItemDao, queueItemDao, true); // <- THIS SHOULD PICK UP EXISTING PARTIAL FILES
841
        // <- THIS ALSO NEEDS TO NOT INTERACT WITH THE INDEX
842
        // <- METADATA UPDATE SHOULD HAPPEN WHEN APP IS INITIALIZED
843

    
844
        // if any download threads were initiated, item is not ready to open
845

    
846
        boolean readyToOpen = true;
847

    
848
        if (newThreads.size() > 0) {
849
            readyToOpen = false;
850

    
851
            // update stored threads for index item
852

    
853
            ArrayList<Thread> currentThreads = downloadThreads.get(eItem.getExpansionId());
854

    
855
            if (currentThreads == null) {
856
                currentThreads = new ArrayList<Thread>();
857
            }
858

    
859
            for (Thread thread : newThreads.values()) {
860
                currentThreads.add(thread);
861
            }
862

    
863
            downloadThreads.put(eItem.getExpansionId(), currentThreads);
864
        }
865

    
866
        if (!installedIds.containsKey(eItem.getExpansionId())) {
867

    
868
            // if clicked item is not installed, update index
869
            // un-installed AvailableIndexItems need to be converted to InstalledIndexItems
870
            InstalledIndexItem iItem = new InstalledIndexItem(eItem);
871

    
872
            java.util.Date thisDate = new java.util.Date(); // FIXME we need to move this somewhere in the liger library that handles opening storypaths because this won't be triggered if we open open an instance by means other than a direct click
873
            iItem.setCreationDate(thisDate);
874
            iItem.setLastModifiedDate(thisDate);
875
            iItem.setCreationDate(thisDate);
876

    
877
            StorymakerIndexManager.installedIndexAdd(BaseHomeActivity.this, iItem, installedIndexItemDao);
878

    
879
            Timber.d(eItem.getExpansionId() + " NOT INSTALLED, ADDING ITEM TO INDEX");
880

    
881
            // wait for index serialization
882
            try {
883
                synchronized (this) {
884
                    wait(1000); // FIXME holy race conditions, batman
885
                }
886
            } catch (InterruptedException e) {
887
                // nop
888
            }
889
        } else {
890

    
891
            Timber.d(eItem.getExpansionId() + " INSTALLED, CHECKING FILE");
892

    
893
            // if clicked item is installed, check state
894
            if (readyToOpen) {
895

    
896
                // clear saved threads
897
                if (downloadThreads.get(eItem.getExpansionId()) != null) {
898
                    downloadThreads.remove(eItem.getExpansionId());
899
                }
900

    
901
                // update db record with flag
902
                if (!eItem.isInstalled()) {
903
                    Timber.d("SET INSTALLED FLAG FOR " + eItem.getExpansionId());
904
                    eItem.setInstalledFlag(true);
905
                    InstalledIndexItem iItem = new InstalledIndexItem(eItem);
906
                    StorymakerIndexManager.installedIndexAdd(this, iItem, installedIndexItemDao);
907
                }
908

    
909
                // if file has been downloaded, open file
910
                Timber.d(eItem.getExpansionId() + " INSTALLED, FILE OK");
911

    
912
                // update with new thumbnail path
913
                // move this somewhere that it can be triggered by completed download?
914
                ContentPackMetadata metadata = scal.io.liger.IndexManager.loadContentMetadata(BaseHomeActivity.this,
915
                        eItem.getPackageName(),
916
                        eItem.getExpansionId(),
917
                        StoryMakerApp.getCurrentLocale().getLanguage());
918

    
919
                if (metadata == null) {
920
                    Toast.makeText(BaseHomeActivity.this, getString(R.string.home_metadata_missing), Toast.LENGTH_LONG).show();
921
                    Timber.e("failed to load content metadata");
922
                } else if ((eItem.getThumbnailPath() == null) || (!eItem.getThumbnailPath().equals(metadata.getContentPackThumbnailPath()))) { // FIXME use StringUtils
923

    
924
                    Timber.d(eItem.getExpansionId() + " FIRST OPEN, UPDATING THUMBNAIL PATH");
925

    
926
                    eItem.setThumbnailPath(metadata.getContentPackThumbnailPath());
927

    
928
                    // un-installed AvailableIndexItems need to be converted to InstalledIndexItems
929
                    InstalledIndexItem iItem = new InstalledIndexItem(eItem);
930
                    StorymakerIndexManager.installedIndexAdd(BaseHomeActivity.this, iItem, installedIndexItemDao);
931

    
932
                    // wait for index serialization
933
                    try {
934
                        synchronized (this) {
935
                            wait(1000); // FIXME holy race conditions, batman
936
                        }
937
                    } catch (InterruptedException e) {
938
                        // nop
939
                    }
940
                }
941

    
942
                ArrayList<scal.io.liger.model.InstanceIndexItem> contentIndex = scal.io.liger.IndexManager.loadContentIndexAsList(BaseHomeActivity.this,
943
                        eItem.getPackageName(),
944
                        eItem.getExpansionId(),
945
                        StoryMakerApp.getCurrentLocale().getLanguage());
946

    
947
                if ((contentIndex == null) || (contentIndex.size() < 1)) {
948
                    Toast.makeText(BaseHomeActivity.this, getString(R.string.home_index_missing), Toast.LENGTH_LONG).show();
949
                    Timber.e("failed to load content index");
950
                } else if (contentIndex.size() == 1) {
951
                    launchLiger(BaseHomeActivity.this, null, null, contentIndex.get(0).getInstanceFilePath());
952
                } else {
953
                    String[] names = new String[contentIndex.size()];
954
                    String[] paths = new String[contentIndex.size()];
955
                    int i = 0;
956
                    for (scal.io.liger.model.InstanceIndexItem item : contentIndex) {
957
                        names[i] = item.getTitle();
958
                        paths[i] = item.getInstanceFilePath();
959
                        i++;
960
                    }
961
                    showSPLSelectorPopup(names, paths);
962
                }
963
            } else {
964
                // if file is being downloaded, don't open
965
                Timber.d(eItem.getExpansionId() + " INSTALLED, CURRENTLY DOWNLOADING FILE");
966

    
967
                // if necessary, un-flag db record (this probably indicates an installed file that is being patched
968
                if (eItem.isInstalled()) {
969
                    Timber.d("UN-SET INSTALLED FLAG FOR " + eItem.getExpansionId());
970
                    eItem.setInstalledFlag(false);
971
                    InstalledIndexItem iItem = new InstalledIndexItem(eItem);
972
                    StorymakerIndexManager.installedIndexAdd(this, iItem, installedIndexItemDao);
973
                }
974

    
975
                // create pause/cancel dialog
976

    
977
                if (showDialog) {
978
                    new AlertDialog.Builder(BaseHomeActivity.this)
979
                            .setTitle(R.string.stop_download)
980
                            .setMessage(eItem.getTitle())
981
                            .setNegativeButton(getString(R.string.cancel), null)
982
                            .setNeutralButton(getString(R.string.pause), new PauseListener(eItem))
983
                            .setPositiveButton(getString(R.string.stop), new CancelListener(eItem))
984
                            .show();
985
                }
986

    
987
                // Toast.makeText(HomeActivity.this, "Please wait for this content pack to finish downloading", Toast.LENGTH_LONG).show(); // FIXME move to strings.xml
988
            }
989
        }
990

    
991

    
992
    }
993

    
994

    
995
    public class CancelListener implements DialogInterface.OnClickListener {
996

    
997
        private ExpansionIndexItem eItem;
998

    
999
        public CancelListener(ExpansionIndexItem eItem) {
1000
            super();
1001

    
1002
            this.eItem = eItem;
1003
        }
1004

    
1005
        @Override
1006
        public void onClick(DialogInterface dialog, int which) {
1007

    
1008
            Timber.d("CANCEL...");
1009
            // remove from installed index
1010

    
1011
            // un-installed AvailableIndexItems need to be converted to InstalledIndexItems
1012
            InstalledIndexItem iItem = new InstalledIndexItem(eItem);
1013
            StorymakerIndexManager.installedIndexRemove(BaseHomeActivity.this, iItem, installedIndexItemDao);
1014

    
1015
            // stop associated threads and delete associated files
1016

    
1017
            ArrayList<Thread> currentThreads = downloadThreads.get(eItem.getExpansionId());
1018

    
1019
            if (currentThreads != null) {
1020
                for (Thread thread : currentThreads) {
1021
                    Timber.d("STOPPING THREAD " + thread.getId());
1022
                    thread.interrupt();
1023
                }
1024
            }
1025

    
1026
            downloadThreads.remove(eItem.getExpansionId());
1027

    
1028
            Timber.d("DELETE STUFF?");
1029

    
1030
            File fileDirectory = StorageHelper.getActualStorageDirectory(BaseHomeActivity.this);
1031
            WildcardFileFilter fileFilter = new WildcardFileFilter(eItem.getExpansionId() + ".*");
1032
            for (File foundFile : FileUtils.listFiles(fileDirectory, fileFilter, null)) {
1033
                Timber.d("STOPPED THREAD: FOUND " + foundFile.getPath() + ", DELETING");
1034
                FileUtils.deleteQuietly(foundFile);
1035
            }
1036
        }
1037
    }
1038

    
1039
    public void updateInstanceIndexItemLastOpenedDate(InstanceIndexItem item) {
1040
        java.util.Date thisDate = new java.util.Date();
1041
        //Log.d("BaseHomeActivity", "setLastOpenedDate " + thisDate.toString());
1042
        instanceIndexItemDao.updateInstanceItemLastOpenedDate(item, thisDate);
1043
    }
1044

    
1045
    //this method loops through a HashMap of ExpansionIndexItems
1046
    //      and returns the ones that have a certain content type i.e. "guide", "lesson", "template"
1047
    //      used to filter an existing query set rather than running extra queries with getAvailableIndexItemsByType() or getInstalledIndexItemsByType()
1048
    public static ArrayList<String> getIndexItemIdsByType(HashMap<String, ExpansionIndexItem> installedIds, String type) {
1049

    
1050
        ArrayList<String> indexItemIds = new ArrayList<String>();
1051

    
1052
        for (String key : installedIds.keySet()) {
1053

    
1054
            ExpansionIndexItem item = installedIds.get(key);
1055

    
1056
            if (StringUtils.equals(item.getContentType(),type)) {
1057
                indexItemIds.add(key);
1058
            }
1059

    
1060
        }
1061

    
1062
        return indexItemIds;
1063

    
1064
    }
1065

    
1066

    
1067
}