Radio Mystery Theater

The new Radio Mystery Theater Android app provides an easy, quality listening experience for the CBS Radio Mystery Theater. 1399 fifty-minute full-cast radio drama episodes are available for listening. The app is unique in presentation and designed for impaired users and commuters. The primary interface is a single button ‘Autoplay’ that controls playback.

When pressed, Autoplay begins with the oldest unheard episode and keeps playing. After one episode finishes, the next one automatically starts. Press Autoplay again to Pause. And again to Resume. Swipe right for next episode, left for previous episode. Long-press to Stop. Once playback begins, a seek ring appears. Use the button on that ring to adjust play position. Settings allow adjusting text size and font for readability. A ticker shows details about the currently playing episode. The app uses Material Design and is very slick. Paid and Trial versions. Companion Wear app.

Radio Mystery Theater app
Download the Paid apk free here

Youtube video here

Question of “Life, the Universe and Everything” is answered!

Mystery of the ages solved. There is a cosmology discussion happening on YouTube where startling speculation began after Lee Hounshell posted:

Zeno’s Paradox is solved. Our universe is digital: There is a smallest unit of time, which cannot be divided. There is also a smallest unit of distance, which cannot be divided. The paradox requires continuous time and continuous distance, which are not representative of our real universe’s physical laws.”

But is our real universe “real” after all?

Comments from the revelation evolved from quantum mechanics into how ‘everything’ (the multiverse) might result from “pure nothing.” The simple, yet bizarre logic is compelling with ideas worth contemplating. Excerpted from the full discussion:


Question of Life the Universe and Everything

Using Gradle for processing Android flavor and buildType with #IFDEF in Java code

If you are a ‘C’ or ‘C++’ programmer you are likely familiar with the #IFDEF syntax used for pre-processing source code. Unfortunately, Java has no pre-processor for managing conditional code like this. When building Android apps, a developer will typically handle dependencies on ‘flavor’ and ‘buildType’ by placing modified versions of source under specially named directories. For example, handling code differences between a ‘paid’ vs. ‘free’ app may lead to 4 versions: paidRelease, freeRelease, paidDebug and freeDebug. Code duplication can become even more severe when more than 2 flavors or more than 2 build types are used. That means app maintenance complexity increases exponentially; the developer must remember to change code similarly in all versions of a duplicated class.

Gradle build rules can be employed to allow a Java source file to contain conditional source for processing all flavor and build type combinations. This post shows you how to do that. I assume you are already familiar with using Gradle and Android Studio for app development.

First create a new file: ‘preprocessor.gradle‘ under your project’s root directory (The directory containing the ‘app’ folder). That file should contain the following code (click to expand):

    // -------------------------------------------------------------------------------------------------
    // Android seems to want to duplicate code using 'buildTypes' and 'flavors' but when minor differences exist
    // it is inefficient to maintain mostly duplicate copies of code this way.  The code below allows java source code
    // to be edited in place. Source sections are commented or uncommented based on specially crafted comments:
    //
    //#IFDEF 'configuration'
    //    java code for the specified 'configuration'
    //#ELSE
    //    java code for NOT the specified 'configuration'
    //#ENDIF
    //
    // The 'configuration' specified above can be a BUILD_TYPE or FLAVOR or BUILD_TYPE+FLAVOR or FLAVOR+BUILD_TYPE
    // For example: 'debug' or 'release' or 'paid' or 'free'
    //              or 'debugpaid' or 'debugfree 'or 'releasepaid' or 'releasefree'
    //              or 'paiddebug' or 'freedebug' or 'paidrelease' or 'freerelease'..
    //              these are all valid 'configuration' entries and will be processed by #IFDEF depending on buildType and flavor.
    // Note that nested #IFDEF statements are not supported (and there is no actual need to nest).
    // Also the 'configuration' is case independent
    //
    // To use this preprocessor, add the following line to your app/build.gradle:
    //     apply from: '../preprocessor.gradle'
    //
    // Then in your java source with build dependencies, do something like this:
    //
    //#IFDEF 'paidRelease'
    //Log.v(TAG, "example of #IFDEF 'paidRelease'");
    //#ELSE
    //Log.v(TAG, "example of NOT #IFDEF 'paidRelease'");
    //#ENDIF
    //
    // Now during a gradle build, the appropriate lines of java code will be commented and uncommented as required.
    //
    // Author: Lee Hounshell - lee.hounshell@gmail.com - Jan 11, 2016
    // See: http://harlie.com/?p=38
    
     
    String sourceDirectory = 'src'
    FileTree javaFiles = fileTree(sourceDirectory) {
        include '**/*.java'
    }
     
    // auto comment and uncomment source lines between #IFDEF 'configuration' and #ELSE or #ENDIF
    // each matching java source file is edited in-place
    class PreProcessor {
     
        public enum IfdefState {
            NONE,
            IFDEF,
            ELSE
        }
     
        public static void preProcessSourceCode (FileTree javaFiles, String buildType, String flavor) {
            buildType = buildType.toLowerCase()
            flavor = flavor.toLowerCase()
            println("---> preProcessSourceCode BUILD_TYPE="+buildType+" FLAVOR="+flavor)
            String buildTypeAndFlavor = buildType + flavor
            String flavorAndBuildType = flavor + buildType
            String ifdefRegex = '^([ ]*)(\\/\\/)#IFDEF \'(.*)\'$'
            String elseRegex = '^([ ]*)(\\/\\/)#ELSE$'
            String endifRegex = '^([ ]*)(\\/\\/)#ENDIF$'
            String lineRegex = '^([ ]*)([^ ][^ ])(.*)$'
            String singleCharLineRegex = '^([ ]*)([^ ])$'
            String comment = "//"
            String newline = System.getProperty("line.separator")
     
            javaFiles.each { File javaFile ->
                println "checking for '$ifdefRegex' in $javaFile.name"
                String content = javaFile.getText()
                StringBuilder newContent = new StringBuilder()
                IfdefState match = IfdefState.NONE
                boolean changed = false;
                String buildTypeAndOrFlavor = "<undefined>"
                content.eachLine { line, index ->
                    // process #IFDEF
                    if (line.matches(ifdefRegex)) {
                        buildTypeAndOrFlavor = (line.split('\'')[1]).toLowerCase()
                        println("--> #IFDEF on line $index for $buildTypeAndOrFlavor")
                        if (buildTypeAndOrFlavor.equals(buildType)) {
                            match = IfdefState.IFDEF
                            println("--> $buildTypeAndOrFlavor IS A MATCH FOR BUILD_TYPE $buildType")
                        }
                        else if (buildTypeAndOrFlavor.equals(flavor)) {
                            match = IfdefState.IFDEF
                            println("--> $buildTypeAndOrFlavor IS A MATCH FOR FLAVOR $flavor")
                        }
                        else if (buildTypeAndOrFlavor.equals(buildTypeAndFlavor)) {
                            match = IfdefState.IFDEF
                            println("--> $buildTypeAndOrFlavor IS A MATCH FOR COMBO BUILD_TYPE PLUS FLAVOR $buildTypeAndFlavor")
                        }
                        else if (buildTypeAndOrFlavor.equals(flavorAndBuildType)) {
                            match = IfdefState.IFDEF
                            println("--> $buildTypeAndOrFlavor IS A MATCH FOR COMBO FLAVOR PLUS BUILD_TYPE $flavorAndBuildType")
                        }
                        else {
                            match = IfdefState.ELSE
                            println("--> $buildTypeAndOrFlavor IS NOT A MATCH FOR BUILD_TYPE $buildType OR FLAVOR $flavor OR COMBO $buildTypeAndFlavor OR COMBO $flavorAndBuildType")
                        }
                    }
                    // process #ELSE
                    else if (line.matches(elseRegex)) {
                        println("--> #ELSE on line $index for $buildTypeAndOrFlavor")
                        if (match != IfdefState.ELSE) {
                            match = IfdefState.ELSE
                            println("--> $buildTypeAndOrFlavor IS NOT A MATCH FOR #ELSE")
                        }
                        else {
                            match = IfdefState.IFDEF
                            println("--> $buildTypeAndOrFlavor IS A MATCH FOR #ELSE")
                        }
                    }
                    // process #ENDIF
                    else if (line.matches(endifRegex)) {
                        println("--> #ENDIF on line $index for $buildTypeAndOrFlavor")
                        match = IfdefState.NONE
                    }
                    // comment or uncomment code or leave it unchanged
                    else {
                        if (match == IfdefState.IFDEF) { // ifdef: uncomment lines up to #ELSE or #ENDIF, as needed
                            if (line.matches(lineRegex)) {
                                def matcher = line =~ lineRegex
                                if (matcher[0][2].equals(comment)) {
                                    line = matcher[0][1] + matcher[0][3]
                                    changed = true
                                    println(line)
                                }
                            }
                        } else if (match == IfdefState.ELSE) { // else: comment-out lines to #ELSE or #ENDIF, as needed
                            if (line.matches(lineRegex)) {
                                def matcher = line =~ lineRegex
                                if (!matcher[0][2].equals(comment)) {
                                    line = matcher[0][1] + comment + matcher[0][2] + matcher[0][3]
                                    changed = true
                                    println(line)
                                }
                            }
                            else if (line.matches(singleCharLineRegex)) {
                                def matcher = line =~ singleCharLineRegex
                                if (!matcher[0][2].equals(comment)) {
                                    line = matcher[0][1] + comment + matcher[0][2]
                                    changed = true
                                    println(line)
                                }
                            }
                        }
                    }
                    newContent.append(line + newline)
                }
                // save the file if was edited
                if (changed) {
                    println("==> EDITING THE FILE <==")
                    javaFile.setText(newContent.toString())
                }
            }
        }
     
    }
     
    task preProcessSourceCodeDebugFree << {
        logger.quiet("---> PreProcessor.preProcessSourceCode(javaFiles, 'debug', 'free')")
        description("preprocess free code after //#IFDEF 'debug' to //#ENDIF")
        PreProcessor.preProcessSourceCode(javaFiles, 'debug', 'free')
    }
     
    task preProcessSourceCodeDebugPaid << {
        logger.quiet("---> PreProcessor.preProcessSourceCode(javaFiles, 'debug', 'paid')")
        description("preprocess paid code after //#IFDEF 'debug' to //#ENDIF")
        PreProcessor.preProcessSourceCode(javaFiles, 'debug', 'paid')
    }
     
    task preProcessSourceCodeReleaseFree << {
        logger.quiet("---> PreProcessor.preProcessSourceCode(javaFiles, 'release', 'free')")
        description("preprocess free code after //#IFDEF 'release' to //#ENDIF")
        PreProcessor.preProcessSourceCode(javaFiles, 'release', 'free')
    }
     
    task preProcessSourceCodeReleasePaid << {
        logger.quiet("---> PreProcessor.preProcessSourceCode(javaFiles, 'release', 'paid')")
        description("preprocess paid code after //#IFDEF 'release' to //#ENDIF")
        PreProcessor.preProcessSourceCode(javaFiles, 'release', 'paid')
    }
     
    tasks.whenTaskAdded { task ->
        if (task.name == 'compileFreeDebugJavaWithJavac') {
            logger.quiet('---> compileFreeDebugJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeDebugFree
            preProcessSourceCodeDebugFree.outputs.upToDateWhen { false } // always run
        }
        else if (task.name == 'compileFreeReleaseJavaWithJavac') {
            logger.quiet('---> compileFreeReleaseJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeReleaseFree
            preProcessSourceCodeReleaseFree.outputs.upToDateWhen { false } // always run
        }
        if (task.name == 'compilePaidDebugJavaWithJavac') {
            logger.quiet('---> compilePaidDebugJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeDebugPaid
            preProcessSourceCodeDebugPaid.outputs.upToDateWhen { false } // always run
        }
        else if (task.name == 'compilePaidReleaseJavaWithJavac') {
            logger.quiet('---> compilePaidReleaseJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeReleasePaid
            preProcessSourceCodeReleasePaid.outputs.upToDateWhen { false } // always run
        }
        else if (task.name == 'compileFreeDebugUnitTestJavaWithJavac') {
            logger.quiet('---> compileFreeDebugUnitTestJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeDebugFree
            preProcessSourceCodeDebugFree.outputs.upToDateWhen { false } // always run
        }
        else if (task.name == 'compileFreeReleaseUnitTestJavaWithJavac') {
            logger.quiet('---> compileFreeReleaseUnitTestJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeReleaseFree
            preProcessSourceCodeReleaseFree.outputs.upToDateWhen { false } // always run
        }
        else if (task.name == 'compilePaidDebugUnitTestJavaWithJavac') {
            logger.quiet('---> compilePaidDebugUnitTestJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeDebugPaid
            preProcessSourceCodeDebugPaid.outputs.upToDateWhen { false } // always run
        }
        else if (task.name == 'compilePaidReleaseUnitTestJavaWithJavac') {
            logger.quiet('---> compilePaidReleaseUnitTestJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeReleasePaid
            preProcessSourceCodeReleasePaid.outputs.upToDateWhen { false } // always run
        }
        else if (task.name == 'compileFreeDebugAndroidTestJavaWithJavac') {
            logger.quiet('---> compileFreeDebugAndroidTestJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeDebugFree
            preProcessSourceCodeDebugFree.outputs.upToDateWhen { false } // always run
        }
        else if (task.name == 'compileFreeReleaseAndroidTestJavaWithJavac') {
            logger.quiet('---> compileFreeReleaseAndroidTestJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeReleaseFree
            preProcessSourceCodeReleaseFree.outputs.upToDateWhen { false } // always run
        }
        else if (task.name == 'compilePaidDebugAndroidTestJavaWithJavac') {
            logger.quiet('---> compilePaidDebugAndroidTestJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeDebugPaid
            preProcessSourceCodeDebugPaid.outputs.upToDateWhen { false } // always run
        }
        else if (task.name == 'compilePaidReleaseAndroidTestJavaWithJavac') {
            logger.quiet('---> compilePaidReleaseAndroidTestJavaWithJavac dependsOn preProcessSourceCode')
            task.dependsOn preProcessSourceCodeReleasePaid
            preProcessSourceCodeReleasePaid.outputs.upToDateWhen { false } // always run
        }
    }

The code above works by allowing gradle to modify your source code, in place, when a build is run. Source code for your app will be dynamically edited to reflect the current settings for flavor and buildType. Now modify your ‘app/build.gradle‘ and add the line:

apply from: '../preprocessor.gradle'

so that your build includes the new preprocessor.gradle rules. Below is an example showing what the ‘app/build.gradle‘ file might look like if using free and paid builds. Important: Only the second line in the example shown below is needed to pull in the new build rules. This example shows a full build configuration only for completeness:

    apply plugin: 'com.android.application'
    apply from: '../preprocessor.gradle'
    
    android {
    
        ext.addDependency = {
            task, flavor, dependency ->
                println('task='+(String)task+'flavor='+(String)flavor+'dependency='+(String)dependency)
        }
    
        if (project.hasProperty("MyProject.properties")
                && new File(project.property("MyProject.properties") as String).exists()) {
    
            Properties props = new Properties()
            props.load(new FileInputStream(file(project.property("MyProject.properties"))))
    
            signingConfigs {
                release {
                    keyAlias props['keystore.alias']
                    keyPassword props['keystore.password']
                    storeFile file(props['keystore'])
                    storePassword props['keystore.password']
                }
                debug {
                    keyAlias props['keystore.alias']
                    keyPassword props['keystore.password']
                    storeFile file(props['keystore'])
                    storePassword props['keystore.password']
                }
            }
        }
    
        compileSdkVersion 'Google Inc.:Google APIs:23'
        buildToolsVersion "23.0.2"
    
        defaultConfig {
            applicationId "com.example.builditbigger"
            minSdkVersion 14
            targetSdkVersion 23
            versionCode 1
            versionName "1.0"
        }
    
        buildTypes {
            debug {
                debuggable true
            }
            release {
                minifyEnabled true
                shrinkResources true
                proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
                signingConfig signingConfigs.release
            }
        }
    
        productFlavors {
            paid {
                applicationId "com.example.builditbigger.paid"
                versionName "1.0-Paid"
            }
            free {
                applicationId "com.example.builditbigger.free"
                versionName "1.0-Free"
            }
        }
    
    }
    
    repositories {
        mavenCentral()
        jcenter()
    }
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        compile 'com.android.support:appcompat-v7:23.1.1'
        compile 'com.android.support:design:23.1.1'
        // Added for AdMob
        freeCompile 'com.google.android.gms:play-services:8.4.0'
        freeCompile 'com.google.android.gms:play-services-ads:8.4.0'
    }
    

Now your app is ready to use #IFDEF #ELSE and #ENDIF when compiling Java code. Because Java does not recognize these new keywords, we need to prefix them with ‘//‘ so they are marked as comments. Here is an example ‘CheckPlayStore.java‘ class showing how that is done. In this example, the ImageView upgrade_paid will be null for release builds and non-null for free builds (assuming of course that your free layout.xml contains an ImageView with id ‘upgrade_to_paid‘).  This means the block of code after

if (upgrade_paid != null)

will only execute for free builds. The included Logging examples also show how to conditionally check for both flavor and buildType together. Note that the #IFDEF ‘configuration’ directive is case-independent:

    package com.example.builditbigger.util;
    
    import android.app.Activity;
    import android.content.ActivityNotFoundException;
    import android.content.Context;
    import android.content.Intent;
    import android.content.pm.PackageInfo;
    import android.content.pm.PackageManager;
    import android.net.Uri;
    import android.util.Log;
    import android.view.View;
    import android.widget.ImageView;
    
    import com.example.builditbigger.R;
    
    public class CheckPlayStore {
        private final static String TAG = "EXAMPLE: <" + CheckPlayStore.class.getSimpleName() + ">";
    
        public static void upgradeToPaid(final Activity activity) {
            @SuppressWarnings("UnusedAssignment") ImageView upgrade_paid = null;
    
            //---------------------------------------------------------------------------------------------------------
            // IMPORTANT NOTE: the following #IFDEF #ELSE and #ENDIF directives are processed in build.gradle prior to javac
            //                 CODE IN THIS BLOCK DEPENDS ON 'BUILD_TYPE' AND/OR 'FLAVOR' AND IS DYNAMICALLY EDITED BY GRADLE
            //---------------------------------------------------------------------------------------------------------
            //#IFDEF 'free'
            //upgrade_paid = (ImageView) activity.findViewById(R.id.upgrade_to_paid);
            //#ENDIF
    
            // combo BUILD_TYPE+FLAVOR and FLAVOR+BUILD_TYPE examples..
    
            //#IFDEF 'freeDebug'
            //Log.v(TAG, "example of #IFDEF 'freeDebug'");
            //#ELSE
            //Log.v(TAG, "example of NOT #IFDEF 'freeDebug'");
            //#ENDIF
    
            //#IFDEF 'releaseFree'
            //Log.v(TAG, "example of #IFDEF 'releaseFree'");
            //#ELSE
            //Log.v(TAG, "example of NOT #IFDEF 'releaseFree'");
            //#ENDIF
    
            //#IFDEF 'DEBUGPAID'
            //Log.v(TAG, "example of #IFDEF 'DEBUGPAID'");
            //#ELSE
            //Log.v(TAG, "example of NOT #IFDEF 'DEBUGPAID'");
            //#ENDIF
    
            //#IFDEF 'paidRelease'
            //Log.v(TAG, "example of #IFDEF 'paidRelease'");
            //#ELSE
            //Log.v(TAG, "example of NOT #IFDEF 'paidRelease'");
            //#ENDIF
            //---------------------------------------------------------------------------------------------------------
    
            //noinspection ConstantConditions
            if (upgrade_paid != null) {
                Log.v(TAG, "using FREE version.");
                upgrade_paid.setOnClickListener(new View.OnClickListener() {
                    public void onClick(View v) {
                        String packageId = activity.getApplicationContext().getPackageName();
                        packageId = packageId.replace(".free", ".paid");
                        Log.v(TAG, "packageId="+packageId);
                        try {
                            // from: http://stackoverflow.com/questions/3239478/how-to-link-to-android-market-app
                            String upgradeLink = "http://market.android.com/details?id=" + packageId;
                            if (CheckPlayStore.isGooglePlayInstalled(activity.getApplicationContext())) {
                                upgradeLink = "market://details?id=" + packageId;
                            }
                            Log.v(TAG, "upgradeLink=" + upgradeLink);
                            activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(upgradeLink)));
                        }
                        catch (ActivityNotFoundException e) {
                            Log.e(TAG, "APP id='"+packageId+"' NOT FOUND ON PLAYSTORE!");
                        }
                    }
                });
            }
            else {
                Log.v(TAG, "using PAID version.");
            }
        }
    
        // from: http://stackoverflow.com/questions/15401748/how-to-detect-if-google-play-is-installed-not-market
        private static boolean isGooglePlayInstalled(Context context) {
            PackageManager pm = context.getPackageManager();
            boolean app_installed;
            try
            {
                PackageInfo info = pm.getPackageInfo("com.android.vending", PackageManager.GET_ACTIVITIES);
                String label = (String) info.applicationInfo.loadLabel(pm);
                app_installed = (label != null && ! label.equals("Market"));
            }
            catch (PackageManager.NameNotFoundException e)
            {
                app_installed = false;
            }
            Log.v(TAG, "isGooglePlayInstalled=" + app_installed);
            return app_installed;
        }
    
    }

enjoy.

Your support for my work is greatly appreciated!