aboutsummaryrefslogtreecommitdiff
path: root/common/src
diff options
context:
space:
mode:
Diffstat (limited to 'common/src')
-rw-r--r--common/src/com/android/tv/common/BaseApplication.java107
-rw-r--r--common/src/com/android/tv/common/BaseSingletons.java35
-rw-r--r--common/src/com/android/tv/common/BooleanSystemProperty.java16
-rw-r--r--common/src/com/android/tv/common/CommonConstants.java28
-rw-r--r--common/src/com/android/tv/common/CommonPreferenceProvider.java215
-rw-r--r--common/src/com/android/tv/common/CommonPreferences.java332
-rw-r--r--common/src/com/android/tv/common/SoftPreconditions.java151
-rw-r--r--common/src/com/android/tv/common/TvCommonConstants.java68
-rw-r--r--common/src/com/android/tv/common/TvCommonUtils.java74
-rw-r--r--common/src/com/android/tv/common/TvContentRatingCache.java34
-rw-r--r--common/src/com/android/tv/common/WeakHandler.java9
-rw-r--r--common/src/com/android/tv/common/actions/InputSetupActionUtils.java127
-rw-r--r--common/src/com/android/tv/common/annotation/UsedByNative.java43
-rw-r--r--common/src/com/android/tv/common/concurrent/NamedThreadFactory.java45
-rw-r--r--common/src/com/android/tv/common/config/DefaultConfigManager.java60
-rw-r--r--common/src/com/android/tv/common/config/RemoteConfigFeature.java42
-rw-r--r--common/src/com/android/tv/common/config/api/RemoteConfig.java54
-rw-r--r--common/src/com/android/tv/common/config/api/RemoteConfigValue.java48
-rw-r--r--common/src/com/android/tv/common/customization/CustomAction.java68
-rw-r--r--common/src/com/android/tv/common/customization/CustomizationManager.java273
-rw-r--r--common/src/com/android/tv/common/experiments/ExperimentFlag.java67
-rw-r--r--common/src/com/android/tv/common/experiments/ExperimentLoader.java (renamed from common/src/com/android/tv/common/annotation/UsedByReflection.java)19
-rw-r--r--common/src/com/android/tv/common/experiments/Experiments.java63
-rw-r--r--common/src/com/android/tv/common/feature/CommonFeatures.java71
-rw-r--r--common/src/com/android/tv/common/feature/EngOnlyFeature.java7
-rw-r--r--common/src/com/android/tv/common/feature/ExperimentFeature.java44
-rw-r--r--common/src/com/android/tv/common/feature/Feature.java13
-rw-r--r--common/src/com/android/tv/common/feature/FeatureUtils.java74
-rw-r--r--common/src/com/android/tv/common/feature/GServiceFeature.java5
-rw-r--r--common/src/com/android/tv/common/feature/Model.java48
-rw-r--r--common/src/com/android/tv/common/feature/PackageVersionFeature.java7
-rw-r--r--common/src/com/android/tv/common/feature/PropertyFeature.java17
-rw-r--r--common/src/com/android/tv/common/feature/Sdk.java14
-rw-r--r--common/src/com/android/tv/common/feature/SharedPreferencesFeature.java17
-rw-r--r--common/src/com/android/tv/common/feature/SystemAppFeature.java6
-rw-r--r--common/src/com/android/tv/common/feature/TestableFeature.java38
-rw-r--r--common/src/com/android/tv/common/memory/MemoryManageable.java (renamed from common/src/com/android/tv/common/MemoryManageable.java)11
-rw-r--r--common/src/com/android/tv/common/recording/RecordingCapability.java88
-rw-r--r--common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java256
-rw-r--r--common/src/com/android/tv/common/ui/setup/OnActionClickListener.java8
-rw-r--r--common/src/com/android/tv/common/ui/setup/SetupActionHelper.java51
-rw-r--r--common/src/com/android/tv/common/ui/setup/SetupActivity.java49
-rw-r--r--common/src/com/android/tv/common/ui/setup/SetupFragment.java128
-rw-r--r--common/src/com/android/tv/common/ui/setup/SetupGuidedStepFragment.java139
-rw-r--r--common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java60
-rw-r--r--common/src/com/android/tv/common/ui/setup/animation/FadeAndShortSlide.java127
-rw-r--r--common/src/com/android/tv/common/ui/setup/animation/SetupAnimationHelper.java96
-rw-r--r--common/src/com/android/tv/common/ui/setup/animation/TranslationAnimationCreator.java42
-rw-r--r--common/src/com/android/tv/common/util/AutoCloseableUtils.java (renamed from common/src/com/android/tv/common/AutoCloseableUtils.java)8
-rw-r--r--common/src/com/android/tv/common/util/Clock.java78
-rw-r--r--common/src/com/android/tv/common/util/CollectionUtils.java (renamed from common/src/com/android/tv/common/CollectionUtils.java)36
-rw-r--r--common/src/com/android/tv/common/util/CommonUtils.java154
-rw-r--r--common/src/com/android/tv/common/util/ContentUriUtils.java45
-rw-r--r--common/src/com/android/tv/common/util/Debug.java50
-rw-r--r--common/src/com/android/tv/common/util/DurationTimer.java80
-rw-r--r--common/src/com/android/tv/common/util/LocationUtils.java143
-rw-r--r--common/src/com/android/tv/common/util/NetworkTrafficTags.java63
-rw-r--r--common/src/com/android/tv/common/util/PermissionUtils.java68
-rw-r--r--common/src/com/android/tv/common/util/PostalCodeUtils.java137
-rw-r--r--common/src/com/android/tv/common/util/SharedPreferencesUtils.java (renamed from common/src/com/android/tv/common/SharedPreferencesUtils.java)36
-rw-r--r--common/src/com/android/tv/common/util/StringUtils.java34
-rw-r--r--common/src/com/android/tv/common/util/SystemProperties.java53
-rw-r--r--common/src/com/android/tv/common/util/SystemPropertiesProxy.java79
63 files changed, 3743 insertions, 715 deletions
diff --git a/common/src/com/android/tv/common/BaseApplication.java b/common/src/com/android/tv/common/BaseApplication.java
new file mode 100644
index 00000000..71c9b4d7
--- /dev/null
+++ b/common/src/com/android/tv/common/BaseApplication.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.common;
+
+import android.annotation.TargetApi;
+import android.app.Application;
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.StrictMode;
+import android.support.annotation.VisibleForTesting;
+import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.recording.RecordingStorageStatusManager;
+import com.android.tv.common.util.Clock;
+import com.android.tv.common.util.CommonUtils;
+import com.android.tv.common.util.Debug;
+import com.android.tv.common.util.SystemProperties;
+
+/** The base application class for Live TV applications. */
+public abstract class BaseApplication extends Application implements BaseSingletons {
+ private RecordingStorageStatusManager mRecordingStorageStatusManager;
+
+ /**
+ * An instance of {@link BaseSingletons}. Note that this can be set directly only for the test
+ * purpose.
+ */
+ @VisibleForTesting public static BaseSingletons sSingletons;
+
+ /** Returns the {@link BaseSingletons} using the application context. */
+ public static BaseSingletons getSingletons(Context context) {
+ // STOP-SHIP: changing the method to protected once the Tuner application is created.
+ // No need to be "synchronized" because this doesn't create any instance.
+ if (sSingletons == null) {
+ sSingletons = (BaseSingletons) context.getApplicationContext();
+ }
+ return sSingletons;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).start();
+ Debug.getTimer(Debug.TAG_START_UP_TIMER)
+ .log("Start " + this.getClass().getSimpleName() + ".onCreate");
+ CommonPreferences.initialize(this);
+
+ // Only set StrictMode for ENG builds because the build server only produces userdebug
+ // builds.
+ if (BuildConfig.ENG && SystemProperties.ALLOW_STRICT_MODE.getValue()) {
+ StrictMode.ThreadPolicy.Builder threadPolicyBuilder =
+ new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog();
+ // TODO(b/69565157): Turn penaltyDeath on for VMPolicy when tests are fixed.
+ StrictMode.VmPolicy.Builder vmPolicyBuilder =
+ new StrictMode.VmPolicy.Builder().detectAll().penaltyLog();
+
+ if (!CommonUtils.isRunningInTest()) {
+ threadPolicyBuilder.penaltyDialog();
+ }
+ StrictMode.setThreadPolicy(threadPolicyBuilder.build());
+ StrictMode.setVmPolicy(vmPolicyBuilder.build());
+ }
+ if (CommonFeatures.DVR.isEnabled(this)) {
+ getRecordingStorageStatusManager();
+ }
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ // Fetch remote config
+ getRemoteConfig().fetch(null);
+ return null;
+ }
+ }.execute();
+ }
+
+ @Override
+ public Clock getClock() {
+ return Clock.SYSTEM;
+ }
+
+ /** Returns {@link RecordingStorageStatusManager}. */
+ @Override
+ @TargetApi(Build.VERSION_CODES.N)
+ public RecordingStorageStatusManager getRecordingStorageStatusManager() {
+ if (mRecordingStorageStatusManager == null) {
+ mRecordingStorageStatusManager = new RecordingStorageStatusManager(this);
+ }
+ return mRecordingStorageStatusManager;
+ }
+
+ @Override
+ public abstract Intent getTunerSetupIntent(Context context);
+}
diff --git a/common/src/com/android/tv/common/BaseSingletons.java b/common/src/com/android/tv/common/BaseSingletons.java
new file mode 100644
index 00000000..e735cdb4
--- /dev/null
+++ b/common/src/com/android/tv/common/BaseSingletons.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.common;
+
+import android.content.Context;
+import android.content.Intent;
+import com.android.tv.common.config.api.RemoteConfig.HasRemoteConfig;
+import com.android.tv.common.recording.RecordingStorageStatusManager;
+import com.android.tv.common.util.Clock;
+
+/** Injection point for the base app */
+public interface BaseSingletons extends HasRemoteConfig {
+
+ Clock getClock();
+
+ RecordingStorageStatusManager getRecordingStorageStatusManager();
+
+ Intent getTunerSetupIntent(Context context);
+
+ String getEmbeddedTunerInputId();
+}
diff --git a/common/src/com/android/tv/common/BooleanSystemProperty.java b/common/src/com/android/tv/common/BooleanSystemProperty.java
index 21e575a1..5436524f 100644
--- a/common/src/com/android/tv/common/BooleanSystemProperty.java
+++ b/common/src/com/android/tv/common/BooleanSystemProperty.java
@@ -17,7 +17,6 @@
package com.android.tv.common;
import android.util.Log;
-
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
@@ -25,16 +24,15 @@ import java.util.List;
/**
* Lazy loaded boolean system property.
*
- * <p>Set with <code>adb shell setprop <em>key</em> <em>value</em></code> where:
- * Values 'n', 'no', '0', 'false' or 'off' are considered false.
- * Values 'y', 'yes', '1', 'true' or 'on' are considered true.
- * (case sensitive). See <a href=
+ * <p>Set with <code>adb shell setprop <em>key</em> <em>value</em></code> where: Values 'n', 'no',
+ * '0', 'false' or 'off' are considered false. Values 'y', 'yes', '1', 'true' or 'on' are considered
+ * true. (case sensitive). See <a href=
* "https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/SystemProperties.java"
* >android.os.SystemProperties.getBoolean</a>.
*/
public class BooleanSystemProperty {
- private final static String TAG = "BooleanSystemProperty";
- private final static boolean DEBUG = false;
+ private static final String TAG = "BooleanSystemProperty";
+ private static final boolean DEBUG = false;
private static final List<BooleanSystemProperty> ALL_PROPERTIES = new ArrayList<>();
private final boolean mDefaultValue;
private final String mKey;
@@ -78,7 +76,7 @@ public class BooleanSystemProperty {
}
/**
- * Clears the cached value. The next call to getValue will check {@code
+ * Clears the cached value. The next call to getValue will check {@code
* android.os.SystemProperties}.
*/
public void reset() {
@@ -88,7 +86,7 @@ public class BooleanSystemProperty {
/**
* Returns the value of the system property.
*
- * <p>If the value is cached get the value from {@code android.os.SystemProperties} with the
+ * <p>If the value is cached get the value from {@code android.os.SystemProperties} with the
* default set in the constructor.
*/
public boolean getValue() {
diff --git a/common/src/com/android/tv/common/CommonConstants.java b/common/src/com/android/tv/common/CommonConstants.java
new file mode 100644
index 00000000..ac379d18
--- /dev/null
+++ b/common/src/com/android/tv/common/CommonConstants.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.common;
+
+/** Constants for common use in apps and tests. */
+public final class CommonConstants {
+
+ public static final String BASE_PACKAGE =
+ "com.android.tv";
+ /** A constant for the key of the extra data for the app linking intent. */
+ public static final String EXTRA_APP_LINK_CHANNEL_URI = "app_link_channel_uri";
+
+ private CommonConstants() {}
+}
diff --git a/common/src/com/android/tv/common/CommonPreferenceProvider.java b/common/src/com/android/tv/common/CommonPreferenceProvider.java
new file mode 100644
index 00000000..b0be3038
--- /dev/null
+++ b/common/src/com/android/tv/common/CommonPreferenceProvider.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.common;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+
+/**
+ * A content provider for storing common preferences. It's used across TV app and tuner TV inputs.
+ */
+public class CommonPreferenceProvider extends ContentProvider {
+ /** The authority of the provider */
+ public static final String AUTHORITY = CommonConstants.BASE_PACKAGE + ".common.preferences";
+
+ private static final String PATH_PREFERENCES = "preferences";
+
+ private static final int DATABASE_VERSION = 1;
+ private static final String DATABASE_NAME = "usbtuner_preferences.db";
+ private static final String PREFERENCES_TABLE = "preferences";
+
+ private static final int MATCH_PREFERENCE = 1;
+ private static final int MATCH_PREFERENCE_KEY = 2;
+
+ private static final UriMatcher sUriMatcher;
+
+ private DatabaseOpenHelper mDatabaseOpenHelper;
+
+ static {
+ sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ sUriMatcher.addURI(AUTHORITY, "preferences", MATCH_PREFERENCE);
+ sUriMatcher.addURI(AUTHORITY, "preferences/*", MATCH_PREFERENCE_KEY);
+ }
+
+ /**
+ * Builds a Uri that points to a specific preference.
+ *
+ * @param key a key of the preference to point to
+ */
+ public static Uri buildPreferenceUri(String key) {
+ return Preferences.CONTENT_URI.buildUpon().appendPath(key).build();
+ }
+
+ /** Columns definitions for the preferences table. */
+ public interface Preferences {
+
+ /** The content:// style for the preferences table. */
+ Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH_PREFERENCES);
+
+ /** The MIME type of a directory of preferences. */
+ String CONTENT_TYPE = "vnd.android.cursor.dir/preferences";
+
+ /** The MIME type of a single preference. */
+ String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/preferences";
+
+ /**
+ * The ID of this preference.
+ *
+ * <p>This is auto-incremented.
+ *
+ * <p>Type: INTEGER
+ */
+ String _ID = "_id";
+
+ /**
+ * The key of this preference.
+ *
+ * <p>Should be unique.
+ *
+ * <p>Type: TEXT
+ */
+ String COLUMN_KEY = "key";
+
+ /**
+ * The value of this preference.
+ *
+ * <p>Type: TEXT
+ */
+ String COLUMN_VALUE = "value";
+ }
+
+ private static class DatabaseOpenHelper extends SQLiteOpenHelper {
+ public DatabaseOpenHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(
+ "CREATE TABLE "
+ + PREFERENCES_TABLE
+ + " ("
+ + Preferences._ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + Preferences.COLUMN_KEY
+ + " TEXT NOT NULL,"
+ + Preferences.COLUMN_VALUE
+ + " TEXT);");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // No-op
+ }
+ }
+
+ @Override
+ public boolean onCreate() {
+ mDatabaseOpenHelper = new DatabaseOpenHelper(getContext());
+ return true;
+ }
+
+ @Override
+ public Cursor query(
+ Uri uri,
+ String[] projection,
+ String selection,
+ String[] selectionArgs,
+ String sortOrder) {
+ int match = sUriMatcher.match(uri);
+ if (match != MATCH_PREFERENCE && match != MATCH_PREFERENCE_KEY) {
+ throw new UnsupportedOperationException();
+ }
+ SQLiteDatabase db = mDatabaseOpenHelper.getReadableDatabase();
+ Cursor cursor =
+ db.query(
+ PREFERENCES_TABLE,
+ projection,
+ selection,
+ selectionArgs,
+ null,
+ null,
+ sortOrder);
+ cursor.setNotificationUri(getContext().getContentResolver(), uri);
+ return cursor;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ switch (sUriMatcher.match(uri)) {
+ case MATCH_PREFERENCE:
+ return Preferences.CONTENT_TYPE;
+ case MATCH_PREFERENCE_KEY:
+ return Preferences.CONTENT_ITEM_TYPE;
+ default:
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ }
+
+ /**
+ * Inserts a preference row into the preference table.
+ *
+ * <p>If a key is already exists in the table, it removes the old row and inserts a new row.
+ *
+ * @param uri the URL of the table to insert into
+ * @param values the initial values for the newly inserted row
+ * @return the URL of the newly created row
+ */
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ if (sUriMatcher.match(uri) != MATCH_PREFERENCE) {
+ throw new UnsupportedOperationException();
+ }
+ return insertRow(uri, values);
+ }
+
+ private Uri insertRow(Uri uri, ContentValues values) {
+ SQLiteDatabase db = mDatabaseOpenHelper.getWritableDatabase();
+
+ // Remove the old row.
+ db.delete(
+ PREFERENCES_TABLE,
+ Preferences.COLUMN_KEY + " like ?",
+ new String[] {values.getAsString(Preferences.COLUMN_KEY)});
+
+ long rowId = db.insert(PREFERENCES_TABLE, null, values);
+ if (rowId > 0) {
+ Uri rowUri = buildPreferenceUri(values.getAsString(Preferences.COLUMN_KEY));
+ getContext().getContentResolver().notifyChange(rowUri, null);
+ return rowUri;
+ }
+
+ throw new SQLiteException("Failed to insert row into " + uri);
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/common/src/com/android/tv/common/CommonPreferences.java b/common/src/com/android/tv/common/CommonPreferences.java
new file mode 100644
index 00000000..5a94eecb
--- /dev/null
+++ b/common/src/com/android/tv/common/CommonPreferences.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.GuardedBy;
+import android.support.annotation.IntDef;
+import android.support.annotation.MainThread;
+import android.util.Log;
+import com.android.tv.common.CommonPreferenceProvider.Preferences;
+import com.android.tv.common.util.CommonUtils;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.HashMap;
+import java.util.Map;
+
+/** A helper class for setting/getting common preferences across applications. */
+public class CommonPreferences {
+ private static final String TAG = "CommonPreferences";
+
+ private static final String PREFS_KEY_LAUNCH_SETUP = "launch_setup";
+ private static final String PREFS_KEY_STORE_TS_STREAM = "store_ts_stream";
+ private static final String PREFS_KEY_TRICKPLAY_SETTING = "trickplay_setting";
+ private static final String PREFS_KEY_LAST_POSTAL_CODE = "last_postal_code";
+
+ private static final Map<String, Class> sPref2TypeMapping = new HashMap<>();
+
+ static {
+ sPref2TypeMapping.put(PREFS_KEY_TRICKPLAY_SETTING, int.class);
+ sPref2TypeMapping.put(PREFS_KEY_STORE_TS_STREAM, boolean.class);
+ sPref2TypeMapping.put(PREFS_KEY_LAUNCH_SETUP, boolean.class);
+ sPref2TypeMapping.put(PREFS_KEY_LAST_POSTAL_CODE, String.class);
+ }
+
+ private static final String SHARED_PREFS_NAME =
+ CommonConstants.BASE_PACKAGE + ".common.preferences";
+
+ @IntDef({TRICKPLAY_SETTING_NOT_SET, TRICKPLAY_SETTING_DISABLED, TRICKPLAY_SETTING_ENABLED})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TrickplaySetting {}
+
+ /** Trickplay setting is not changed by a user. Trickplay will be enabled in this case. */
+ public static final int TRICKPLAY_SETTING_NOT_SET = -1;
+
+ /** Trickplay setting is disabled. */
+ public static final int TRICKPLAY_SETTING_DISABLED = 0;
+
+ /** Trickplay setting is enabled. */
+ public static final int TRICKPLAY_SETTING_ENABLED = 1;
+
+ @GuardedBy("CommonPreferences.class")
+ private static final Bundle sPreferenceValues = new Bundle();
+
+ private static LoadPreferencesTask sLoadPreferencesTask;
+ private static ContentObserver sContentObserver;
+ private static CommonPreferencesChangedListener sPreferencesChangedListener = null;
+
+ protected static boolean sInitialized;
+
+ /** Listeners for CommonPreferences change. */
+ public interface CommonPreferencesChangedListener {
+ void onCommonPreferencesChanged();
+ }
+
+ /** Initializes the common preferences. */
+ @MainThread
+ public static void initialize(final Context context) {
+ if (sInitialized) {
+ return;
+ }
+ sInitialized = true;
+ if (useContentProvider(context)) {
+ loadPreferences(context);
+ sContentObserver =
+ new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ loadPreferences(context);
+ }
+ };
+ context.getContentResolver()
+ .registerContentObserver(Preferences.CONTENT_URI, true, sContentObserver);
+ } else {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ getSharedPreferences(context);
+ return null;
+ }
+ }.execute();
+ }
+ }
+
+ /** Releases the resources. */
+ public static synchronized void release(Context context) {
+ if (useContentProvider(context) && sContentObserver != null) {
+ context.getContentResolver().unregisterContentObserver(sContentObserver);
+ }
+ setCommonPreferencesChangedListener(null);
+ }
+
+ /** Sets the listener for CommonPreferences change. */
+ public static void setCommonPreferencesChangedListener(
+ CommonPreferencesChangedListener listener) {
+ sPreferencesChangedListener = listener;
+ }
+
+ /**
+ * Loads the preferences from database.
+ *
+ * <p>This preferences is used across processes, so the preferences should be loaded again when
+ * the databases changes.
+ */
+ @MainThread
+ public static void loadPreferences(Context context) {
+ if (sLoadPreferencesTask != null
+ && sLoadPreferencesTask.getStatus() != AsyncTask.Status.FINISHED) {
+ sLoadPreferencesTask.cancel(true);
+ }
+ sLoadPreferencesTask = new LoadPreferencesTask(context);
+ sLoadPreferencesTask.execute();
+ }
+
+ private static boolean useContentProvider(Context context) {
+ // If TIS is a part of LC, it should use ContentProvider to resolve multiple process access.
+ return CommonUtils.isPackagedWithLiveChannels(context);
+ }
+
+ public static synchronized boolean shouldShowSetupActivity(Context context) {
+ SoftPreconditions.checkState(sInitialized);
+ if (useContentProvider(context)) {
+ return sPreferenceValues.getBoolean(PREFS_KEY_LAUNCH_SETUP);
+ } else {
+ return getSharedPreferences(context).getBoolean(PREFS_KEY_LAUNCH_SETUP, false);
+ }
+ }
+
+ public static synchronized void setShouldShowSetupActivity(Context context, boolean need) {
+ if (useContentProvider(context)) {
+ setPreference(context, PREFS_KEY_LAUNCH_SETUP, need);
+ } else {
+ getSharedPreferences(context).edit().putBoolean(PREFS_KEY_LAUNCH_SETUP, need).apply();
+ }
+ }
+
+ public static synchronized @TrickplaySetting int getTrickplaySetting(Context context) {
+ SoftPreconditions.checkState(sInitialized);
+ if (useContentProvider(context)) {
+ return sPreferenceValues.getInt(PREFS_KEY_TRICKPLAY_SETTING, TRICKPLAY_SETTING_NOT_SET);
+ } else {
+ return getSharedPreferences(context)
+ .getInt(PREFS_KEY_TRICKPLAY_SETTING, TRICKPLAY_SETTING_NOT_SET);
+ }
+ }
+
+ public static synchronized void setTrickplaySetting(
+ Context context, @TrickplaySetting int trickplaySetting) {
+ SoftPreconditions.checkState(sInitialized);
+ SoftPreconditions.checkArgument(trickplaySetting != TRICKPLAY_SETTING_NOT_SET);
+ if (useContentProvider(context)) {
+ setPreference(context, PREFS_KEY_TRICKPLAY_SETTING, trickplaySetting);
+ } else {
+ getSharedPreferences(context)
+ .edit()
+ .putInt(PREFS_KEY_TRICKPLAY_SETTING, trickplaySetting)
+ .apply();
+ }
+ }
+
+ public static synchronized boolean getStoreTsStream(Context context) {
+ SoftPreconditions.checkState(sInitialized);
+ if (useContentProvider(context)) {
+ return sPreferenceValues.getBoolean(PREFS_KEY_STORE_TS_STREAM, false);
+ } else {
+ return getSharedPreferences(context).getBoolean(PREFS_KEY_STORE_TS_STREAM, false);
+ }
+ }
+
+ public static synchronized void setStoreTsStream(Context context, boolean shouldStore) {
+ if (useContentProvider(context)) {
+ setPreference(context, PREFS_KEY_STORE_TS_STREAM, shouldStore);
+ } else {
+ getSharedPreferences(context)
+ .edit()
+ .putBoolean(PREFS_KEY_STORE_TS_STREAM, shouldStore)
+ .apply();
+ }
+ }
+
+ public static synchronized String getLastPostalCode(Context context) {
+ SoftPreconditions.checkState(sInitialized);
+ if (useContentProvider(context)) {
+ return sPreferenceValues.getString(PREFS_KEY_LAST_POSTAL_CODE);
+ } else {
+ return getSharedPreferences(context).getString(PREFS_KEY_LAST_POSTAL_CODE, null);
+ }
+ }
+
+ public static synchronized void setLastPostalCode(Context context, String postalCode) {
+ if (useContentProvider(context)) {
+ setPreference(context, PREFS_KEY_LAST_POSTAL_CODE, postalCode);
+ } else {
+ getSharedPreferences(context)
+ .edit()
+ .putString(PREFS_KEY_LAST_POSTAL_CODE, postalCode)
+ .apply();
+ }
+ }
+
+ protected static SharedPreferences getSharedPreferences(Context context) {
+ return context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
+ }
+
+ private static synchronized void setPreference(Context context, String key, String value) {
+ sPreferenceValues.putString(key, value);
+ savePreference(context, key, value);
+ }
+
+ private static synchronized void setPreference(Context context, String key, int value) {
+ sPreferenceValues.putInt(key, value);
+ savePreference(context, key, Integer.toString(value));
+ }
+
+ private static synchronized void setPreference(Context context, String key, long value) {
+ sPreferenceValues.putLong(key, value);
+ savePreference(context, key, Long.toString(value));
+ }
+
+ private static synchronized void setPreference(Context context, String key, boolean value) {
+ sPreferenceValues.putBoolean(key, value);
+ savePreference(context, key, Boolean.toString(value));
+ }
+
+ private static void savePreference(
+ final Context context, final String key, final String value) {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ ContentResolver resolver = context.getContentResolver();
+ ContentValues values = new ContentValues();
+ values.put(Preferences.COLUMN_KEY, key);
+ values.put(Preferences.COLUMN_VALUE, value);
+ try {
+ resolver.insert(Preferences.CONTENT_URI, values);
+ } catch (Exception e) {
+ SoftPreconditions.warn(
+ TAG, "setPreference", e, "Error writing preference values");
+ }
+ return null;
+ }
+ }.execute();
+ }
+
+ private static class LoadPreferencesTask extends AsyncTask<Void, Void, Bundle> {
+ private final Context mContext;
+
+ private LoadPreferencesTask(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ protected Bundle doInBackground(Void... params) {
+ Bundle bundle = new Bundle();
+ ContentResolver resolver = mContext.getContentResolver();
+ String[] projection = new String[] {Preferences.COLUMN_KEY, Preferences.COLUMN_VALUE};
+ try (Cursor cursor =
+ resolver.query(Preferences.CONTENT_URI, projection, null, null, null)) {
+ if (cursor != null) {
+ while (!isCancelled() && cursor.moveToNext()) {
+ String key = cursor.getString(0);
+ String value = cursor.getString(1);
+ Class prefClass = sPref2TypeMapping.get(key);
+ if (prefClass == int.class) {
+ try {
+ bundle.putInt(key, Integer.parseInt(value));
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Invalid format, key=" + key + ", value=" + value);
+ }
+ } else if (prefClass == long.class) {
+ try {
+ bundle.putLong(key, Long.parseLong(value));
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Invalid format, key=" + key + ", value=" + value);
+ }
+ } else if (prefClass == boolean.class) {
+ bundle.putBoolean(key, Boolean.parseBoolean(value));
+ } else {
+ bundle.putString(key, value);
+ }
+ }
+ }
+ } catch (Exception e) {
+ SoftPreconditions.warn(TAG, "getPreference", e, "Error querying preference values");
+ return null;
+ }
+ return bundle;
+ }
+
+ @Override
+ protected void onPostExecute(Bundle bundle) {
+ synchronized (CommonPreferences.class) {
+ if (bundle != null) {
+ sPreferenceValues.putAll(bundle);
+ }
+ }
+ if (sPreferencesChangedListener != null) {
+ sPreferencesChangedListener.onCommonPreferencesChanged();
+ }
+ }
+ }
+}
diff --git a/common/src/com/android/tv/common/SoftPreconditions.java b/common/src/com/android/tv/common/SoftPreconditions.java
index 823c42ff..3b0510d8 100644
--- a/common/src/com/android/tv/common/SoftPreconditions.java
+++ b/common/src/com/android/tv/common/SoftPreconditions.java
@@ -17,17 +17,18 @@
package com.android.tv.common;
import android.content.Context;
+import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
-
import com.android.tv.common.feature.Feature;
+import com.android.tv.common.util.CommonUtils;
/**
- * Simple static methods to be called at the start of your own methods to verify
- * correct arguments and state.
+ * Simple static methods to be called at the start of your own methods to verify correct arguments
+ * and state.
*
- * <p>{@code checkXXX} methods throw exceptions when {@link BuildConfig#ENG} is true, and
- * logs a warning when it is false.
+ * <p>{@code checkXXX} methods throw exceptions when {@link BuildConfig#ENG} is true, and logs a
+ * warning when it is false.
*
* <p>This is based on com.android.internal.util.Preconditions.
*/
@@ -35,26 +36,35 @@ public final class SoftPreconditions {
private static final String TAG = "SoftPreconditions";
/**
- * Throws or logs if an expression involving the parameter of the calling
- * method is not true.
+ * Throws or logs if an expression involving the parameter of the calling method is not true.
*
* @param expression a boolean expression
- * @param tag Used to identify the source of a log message. It usually
- * identifies the class or activity where the log call occurs.
- * @param msg The message you would like logged.
+ * @param tag Used to identify the source of a log message. It usually identifies the class or
+ * activity where the log call occurs.
+ * @param errorMessageTemplate a template for the exception message should the check fail. The
+ * message is formed by replacing each {@code %s} placeholder in the template with an
+ * argument. These are matched by position - the first {@code %s} gets {@code
+ * errorMessageArgs[0]}, etc. Unmatched arguments will be appended to the formatted message
+ * in square braces. Unmatched placeholders will be left as-is.
+ * @param errorMessageArgs the arguments to be substituted into the message template. Arguments
+ * are converted to strings using {@link String#valueOf(Object)}.
* @return the evaluation result of the boolean expression
* @throws IllegalArgumentException if {@code expression} is true
*/
- public static boolean checkArgument(final boolean expression, String tag, String msg) {
+ public static boolean checkArgument(
+ final boolean expression,
+ String tag,
+ @Nullable String errorMessageTemplate,
+ @Nullable Object... errorMessageArgs) {
if (!expression) {
- warn(tag, "Illegal argument", msg, new IllegalArgumentException(msg));
+ String msg = format(errorMessageTemplate, errorMessageArgs);
+ warn(tag, "Illegal argument", new IllegalArgumentException(msg), msg);
}
return expression;
}
/**
- * Throws or logs if an expression involving the parameter of the calling
- * method is not true.
+ * Throws or logs if an expression involving the parameter of the calling method is not true.
*
* @param expression a boolean expression
* @return the evaluation result of the boolean expression
@@ -69,15 +79,26 @@ public final class SoftPreconditions {
* Throws or logs if an and object is null.
*
* @param reference an object reference
- * @param tag Used to identify the source of a log message. It usually
- * identifies the class or activity where the log call occurs.
- * @param msg The message you would like logged.
+ * @param tag Used to identify the source of a log message. It usually identifies the class or
+ * activity where the log call occurs.
+ * @param errorMessageTemplate a template for the exception message should the check fail. The
+ * message is formed by replacing each {@code %s} placeholder in the template with an
+ * argument. These are matched by position - the first {@code %s} gets {@code
+ * errorMessageArgs[0]}, etc. Unmatched arguments will be appended to the formatted message
+ * in square braces. Unmatched placeholders will be left as-is.
+ * @param errorMessageArgs the arguments to be substituted into the message template. Arguments
+ * are converted to strings using {@link String#valueOf(Object)}.
* @return true if the object is null
* @throws NullPointerException if {@code reference} is null
*/
- public static <T> T checkNotNull(final T reference, String tag, String msg) {
+ public static <T> T checkNotNull(
+ final T reference,
+ String tag,
+ @Nullable String errorMessageTemplate,
+ @Nullable Object... errorMessageArgs) {
if (reference == null) {
- warn(tag, "Null Pointer", msg, new NullPointerException(msg));
+ String msg = format(errorMessageTemplate, errorMessageArgs);
+ warn(tag, "Null Pointer", new NullPointerException(msg), msg);
}
return reference;
}
@@ -94,26 +115,37 @@ public final class SoftPreconditions {
}
/**
- * Throws or logs if an expression involving the state of the calling
- * instance, but not involving any parameters to the calling method is not true.
+ * Throws or logs if an expression involving the state of the calling instance, but not
+ * involving any parameters to the calling method is not true.
*
* @param expression a boolean expression
- * @param tag Used to identify the source of a log message. It usually
- * identifies the class or activity where the log call occurs.
- * @param msg The message you would like logged.
+ * @param tag Used to identify the source of a log message. It usually identifies the class or
+ * activity where the log call occurs.
+ * @param errorMessageTemplate a template for the exception message should the check fail. The
+ * message is formed by replacing each {@code %s} placeholder in the template with an
+ * argument. These are matched by position - the first {@code %s} gets {@code
+ * errorMessageArgs[0]}, etc. Unmatched arguments will be appended to the formatted message
+ * in square braces. Unmatched placeholders will be left as-is.
+ * @param errorMessageArgs the arguments to be substituted into the message template. Arguments
+ * are converted to strings using {@link String#valueOf(Object)}.
* @return the evaluation result of the boolean expression
* @throws IllegalStateException if {@code expression} is true
*/
- public static boolean checkState(final boolean expression, String tag, String msg) {
+ public static boolean checkState(
+ final boolean expression,
+ String tag,
+ @Nullable String errorMessageTemplate,
+ @Nullable Object... errorMessageArgs) {
if (!expression) {
- warn(tag, "Illegal State", msg, new IllegalStateException(msg));
+ String msg = format(errorMessageTemplate, errorMessageArgs);
+ warn(tag, "Illegal State", new IllegalStateException(msg), msg);
}
return expression;
}
/**
- * Throws or logs if an expression involving the state of the calling
- * instance, but not involving any parameters to the calling method is not true.
+ * Throws or logs if an expression involving the state of the calling instance, but not
+ * involving any parameters to the calling method is not true.
*
* @param expression a boolean expression
* @return the evaluation result of the boolean expression
@@ -129,8 +161,8 @@ public final class SoftPreconditions {
*
* @param context an android context
* @param feature the required feature
- * @param tag used to identify the source of a log message. It usually
- * identifies the class or activity where the log call occurs
+ * @param tag used to identify the source of a log message. It usually identifies the class or
+ * activity where the log call occurs
* @throws IllegalStateException if {@code feature} is not enabled
*/
public static void checkFeatureEnabled(Context context, Feature feature, String tag) {
@@ -138,14 +170,15 @@ public final class SoftPreconditions {
}
/**
- * Throws a {@link RuntimeException} if {@link BuildConfig#ENG} is true, else log a warning.
+ * Throws a {@link RuntimeException} if {@link BuildConfig#ENG} is true and not running in a
+ * test, else log a warning.
*
- * @param tag Used to identify the source of a log message. It usually
- * identifies the class or activity where the log call occurs.
- * @param msg The message you would like logged
+ * @param tag Used to identify the source of a log message. It usually identifies the class or
+ * activity where the log call occurs.
* @param e The exception to wrap with a RuntimeException when thrown.
+ * @param msg The message to be logged
*/
- public static void warn(String tag, String prefix, String msg, Exception e)
+ public static void warn(String tag, String prefix, Exception e, String msg)
throws RuntimeException {
if (TextUtils.isEmpty(tag)) {
tag = TAG;
@@ -159,13 +192,57 @@ public final class SoftPreconditions {
logMessage = prefix + ": " + msg;
}
- if (BuildConfig.ENG) {
+ if (BuildConfig.ENG && !CommonUtils.isRunningInTest()) {
throw new RuntimeException(msg, e);
} else {
Log.w(tag, logMessage, e);
}
}
- private SoftPreconditions() {
+ /**
+ * Substitutes each {@code %s} in {@code template} with an argument. These are matched by
+ * position: the first {@code %s} gets {@code args[0]}, etc. If there are more arguments than
+ * placeholders, the unmatched arguments will be appended to the end of the formatted message in
+ * square braces.
+ *
+ * @param template a string containing 0 or more {@code %s} placeholders. null is treated as
+ * "null".
+ * @param args the arguments to be substituted into the message template. Arguments are
+ * converted to strings using {@link String#valueOf(Object)}. Arguments can be null.
+ */
+ static String format(@Nullable String template, @Nullable Object... args) {
+ template = String.valueOf(template); // null -> "null"
+
+ args = args == null ? new Object[] {"(Object[])null"} : args;
+
+ // start substituting the arguments into the '%s' placeholders
+ StringBuilder builder = new StringBuilder(template.length() + 16 * args.length);
+ int templateStart = 0;
+ int i = 0;
+ while (i < args.length) {
+ int placeholderStart = template.indexOf("%s", templateStart);
+ if (placeholderStart == -1) {
+ break;
+ }
+ builder.append(template, templateStart, placeholderStart);
+ builder.append(args[i++]);
+ templateStart = placeholderStart + 2;
+ }
+ builder.append(template, templateStart, template.length());
+
+ // if we run out of placeholders, append the extra args in square braces
+ if (i < args.length) {
+ builder.append(" [");
+ builder.append(args[i++]);
+ while (i < args.length) {
+ builder.append(", ");
+ builder.append(args[i++]);
+ }
+ builder.append(']');
+ }
+
+ return builder.toString();
}
+
+ private SoftPreconditions() {}
}
diff --git a/common/src/com/android/tv/common/TvCommonConstants.java b/common/src/com/android/tv/common/TvCommonConstants.java
deleted file mode 100644
index 9824497e..00000000
--- a/common/src/com/android/tv/common/TvCommonConstants.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.common;
-
-import android.media.tv.TvInputInfo;
-import android.os.Build;
-
-/**
- * Constants for common use in TV app and tests.
- */
-public final class TvCommonConstants {
- /**
- * A constant for the key of the extra data for the app linking intent.
- */
- public static final String EXTRA_APP_LINK_CHANNEL_URI = "app_link_channel_uri";
-
- /**
- * An intent action to launch setup activity of a TV input. The intent should include
- * TV input ID in the value of {@link EXTRA_INPUT_ID}. Optionally, given the value of
- * {@link EXTRA_ACTIVITY_AFTER_COMPLETION}, the activity will be launched after the setup
- * activity successfully finishes.
- */
- public static final String INTENT_ACTION_INPUT_SETUP =
- "com.android.tv.action.LAUNCH_INPUT_SETUP";
-
- /**
- * A constant of the key to indicate a TV input ID for the intent action
- * {@link INTENT_ACTION_INPUT_SETUP}.
- *
- * <p>Value type: String
- */
- public static final String EXTRA_INPUT_ID = TvInputInfo.EXTRA_INPUT_ID;
-
- /**
- * A constant of the key for intent to launch actual TV input setup activity used with
- * {@link INTENT_ACTION_INPUT_SETUP}.
- *
- * <p>Value type: Intent (Parcelable)
- */
- public static final String EXTRA_SETUP_INTENT =
- "com.android.tv.extra.SETUP_INTENT";
-
- /**
- * A constant of the key to indicate an Activity launch intent for the intent action
- * {@link INTENT_ACTION_INPUT_SETUP}.
- *
- * <p>Value type: Intent (Parcelable)
- */
- public static final String EXTRA_ACTIVITY_AFTER_COMPLETION =
- "com.android.tv.intent.extra.ACTIVITY_AFTER_COMPLETION";
-
- private TvCommonConstants() {
- }
-}
diff --git a/common/src/com/android/tv/common/TvCommonUtils.java b/common/src/com/android/tv/common/TvCommonUtils.java
deleted file mode 100644
index c391ad24..00000000
--- a/common/src/com/android/tv/common/TvCommonUtils.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.common;
-
-import android.content.Intent;
-import android.media.tv.TvInputInfo;
-
-/**
- * Util class for common use in TV app and inputs.
- */
-public final class TvCommonUtils {
- private static Boolean sRunningInTest;
-
- private TvCommonUtils() { }
-
- /**
- * Returns an intent to start the setup activity for the TV input using {@link
- * TvCommonConstants#INTENT_ACTION_INPUT_SETUP}.
- */
- public static Intent createSetupIntent(Intent originalSetupIntent, String inputId) {
- if (originalSetupIntent == null) {
- return null;
- }
- Intent setupIntent = new Intent(originalSetupIntent);
- if (!TvCommonConstants.INTENT_ACTION_INPUT_SETUP.equals(originalSetupIntent.getAction())) {
- Intent intentContainer = new Intent(TvCommonConstants.INTENT_ACTION_INPUT_SETUP);
- intentContainer.putExtra(TvCommonConstants.EXTRA_SETUP_INTENT, originalSetupIntent);
- intentContainer.putExtra(TvCommonConstants.EXTRA_INPUT_ID, inputId);
- setupIntent = intentContainer;
- }
- return setupIntent;
- }
-
- /**
- * Returns an intent to start the setup activity for this TV input using {@link
- * TvCommonConstants#INTENT_ACTION_INPUT_SETUP}.
- */
- public static Intent createSetupIntent(TvInputInfo input) {
- return createSetupIntent(input.createSetupIntent(), input.getId());
- }
-
- /**
- * Checks if this application is running in tests.
- *
- * <p>{@link android.app.ActivityManager#isRunningInTestHarness} doesn't return {@code true} for
- * the usual devices even the application is running in tests. We need to figure it out by
- * checking whether the class in tv-tests-common module can be loaded or not.
- */
- public static synchronized boolean isRunningInTest() {
- if (sRunningInTest == null) {
- try {
- Class.forName("com.android.tv.testing.Utils");
- sRunningInTest = true;
- } catch (ClassNotFoundException e) {
- sRunningInTest = false;
- }
- }
- return sRunningInTest;
- }
-}
diff --git a/common/src/com/android/tv/common/TvContentRatingCache.java b/common/src/com/android/tv/common/TvContentRatingCache.java
index 8b3c06f1..cfdb8e4d 100644
--- a/common/src/com/android/tv/common/TvContentRatingCache.java
+++ b/common/src/com/android/tv/common/TvContentRatingCache.java
@@ -22,7 +22,7 @@ import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
-
+import com.android.tv.common.memory.MemoryManageable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -31,13 +31,11 @@ import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
-/**
- * TvContentRating cache.
- */
+/** TvContentRating cache. */
public final class TvContentRatingCache implements MemoryManageable {
- private final static String TAG = "TvContentRatings";
+ private static final String TAG = "TvContentRatings";
- private final static TvContentRatingCache INSTANCE = new TvContentRatingCache();
+ private static final TvContentRatingCache INSTANCE = new TvContentRatingCache();
public static TvContentRatingCache getInstance() {
return INSTANCE;
@@ -48,8 +46,8 @@ public final class TvContentRatingCache implements MemoryManageable {
/**
* Returns an array TvContentRatings from a string of comma separated set of rating strings
- * creating each from {@link TvContentRating#unflattenFromString(String)} if needed.
- * Returns {@code null} if the string is empty or contains no valid ratings.
+ * creating each from {@link TvContentRating#unflattenFromString(String)} if needed. Returns
+ * {@code null} if the string is empty or contains no valid ratings.
*/
@Nullable
public synchronized TvContentRating[] getRatings(String commaSeparatedRatings) {
@@ -60,8 +58,8 @@ public final class TvContentRatingCache implements MemoryManageable {
if (mRatingsMultiMap.containsKey(commaSeparatedRatings)) {
tvContentRatings = mRatingsMultiMap.get(commaSeparatedRatings);
} else {
- String normalizedRatings = TextUtils
- .join(",", getSortedSetFromCsv(commaSeparatedRatings));
+ String normalizedRatings =
+ TextUtils.join(",", getSortedSetFromCsv(commaSeparatedRatings));
if (mRatingsMultiMap.containsKey(normalizedRatings)) {
tvContentRatings = mRatingsMultiMap.get(normalizedRatings);
} else {
@@ -76,9 +74,7 @@ public final class TvContentRatingCache implements MemoryManageable {
return tvContentRatings;
}
- /**
- * Returns a sorted array of TvContentRatings from a comma separated string of ratings.
- */
+ /** Returns a sorted array of TvContentRatings from a comma separated string of ratings. */
@VisibleForTesting
static TvContentRating[] stringToContentRatings(String commaSeparatedRatings) {
if (TextUtils.isEmpty(commaSeparatedRatings)) {
@@ -93,8 +89,9 @@ public final class TvContentRatingCache implements MemoryManageable {
Log.e(TAG, "Can't parse the content rating: '" + rating + "'", e);
}
}
- return contentRatings.size() == 0 ?
- null : contentRatings.toArray(new TvContentRating[contentRatings.size()]);
+ return contentRatings.size() == 0
+ ? null
+ : contentRatings.toArray(new TvContentRating[contentRatings.size()]);
}
private static Set<String> getSortedSetFromCsv(String commaSeparatedRatings) {
@@ -118,8 +115,8 @@ public final class TvContentRatingCache implements MemoryManageable {
}
/**
- * Returns a string of each flattened content rating, sorted and concatenated together
- * with a comma.
+ * Returns a string of each flattened content rating, sorted and concatenated together with a
+ * comma.
*/
public static String contentRatingsToString(TvContentRating[] contentRatings) {
if (contentRatings == null || contentRatings.length == 0) {
@@ -141,6 +138,5 @@ public final class TvContentRatingCache implements MemoryManageable {
mRatingsMultiMap.clear();
}
- private TvContentRatingCache() {
- }
+ private TvContentRatingCache() {}
}
diff --git a/common/src/com/android/tv/common/WeakHandler.java b/common/src/com/android/tv/common/WeakHandler.java
index 188f20ec..dbdd4769 100644
--- a/common/src/com/android/tv/common/WeakHandler.java
+++ b/common/src/com/android/tv/common/WeakHandler.java
@@ -20,7 +20,6 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.NonNull;
-
import java.lang.ref.WeakReference;
/**
@@ -37,7 +36,7 @@ public abstract class WeakHandler<T> extends Handler {
private final WeakReference<T> mRef;
/**
- * Constructs a new handler with a weak reference to the given referent using the provided
+ * Constructs a new handler with a weak reference to the given referent using the provided
* Looper instead of the default one.
*
* @param looper The looper, must not be null.
@@ -57,9 +56,7 @@ public abstract class WeakHandler<T> extends Handler {
mRef = new WeakReference<>(ref);
}
- /**
- * Calls {@link #handleMessage(Message, Object)} if the WeakReference is not cleared.
- */
+ /** Calls {@link #handleMessage(Message, Object)} if the WeakReference is not cleared. */
@Override
public final void handleMessage(Message msg) {
T referent = mRef.get();
@@ -74,7 +71,7 @@ public abstract class WeakHandler<T> extends Handler {
*
* <p>If the WeakReference is cleared this method will no longer be called.
*
- * @param msg the message to handle
+ * @param msg the message to handle
* @param referent the referent. Guaranteed to be non null.
*/
protected abstract void handleMessage(Message msg, @NonNull T referent);
diff --git a/common/src/com/android/tv/common/actions/InputSetupActionUtils.java b/common/src/com/android/tv/common/actions/InputSetupActionUtils.java
new file mode 100644
index 00000000..7ba799ed
--- /dev/null
+++ b/common/src/com/android/tv/common/actions/InputSetupActionUtils.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.common.actions;
+
+import android.content.Intent;
+import android.media.tv.TvInputInfo;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+
+/** Constants and static utilities for the Input Setup Action. */
+public class InputSetupActionUtils {
+
+ /**
+ * An intent action to launch setup activity of a TV input.
+ *
+ * <p>The intent should include TV input ID in the value of {@link #EXTRA_INPUT_ID}. Optionally,
+ * given the value of {@link #EXTRA_ACTIVITY_AFTER_COMPLETION}, the activity will be launched
+ * after the setup activity successfully finishes.
+ */
+ public static final String INTENT_ACTION_INPUT_SETUP =
+ "com.android.tv.action.LAUNCH_INPUT_SETUP";
+ /**
+ * A constant of the key to indicate a TV input ID for the intent action {@link
+ * #INTENT_ACTION_INPUT_SETUP}.
+ *
+ * <p>Value type: String
+ */
+ public static final String EXTRA_INPUT_ID = TvInputInfo.EXTRA_INPUT_ID;
+ /**
+ * A constant of the key for intent to launch actual TV input setup activity used with {@link
+ * #INTENT_ACTION_INPUT_SETUP}.
+ *
+ * <p>Value type: Intent (Parcelable)
+ */
+ public static final String EXTRA_SETUP_INTENT = "com.android.tv.extra.SETUP_INTENT";
+ /**
+ * A constant of the key to indicate an Activity launch intent for the intent action {@link
+ * #INTENT_ACTION_INPUT_SETUP}.
+ *
+ * <p>Value type: Intent (Parcelable)
+ */
+ public static final String EXTRA_ACTIVITY_AFTER_COMPLETION =
+ "com.android.tv.intent.extra.ACTIVITY_AFTER_COMPLETION";
+ /**
+ * An intent action to launch setup activity of a TV input.
+ *
+ * <p>The intent should include TV input ID in the value of {@link #EXTRA_INPUT_ID}. Optionally,
+ * given the value of {@link #EXTRA_GOOGLE_ACTIVITY_AFTER_COMPLETION}, the activity will be
+ * launched after the setup activity successfully finishes.
+ *
+ * <p>Value type: Intent (Parcelable)
+ *
+ * @deprecated Use {@link #INTENT_ACTION_INPUT_SETUP} instead
+ */
+ @Deprecated
+ private static final String INTENT_GOOGLE_ACTION_INPUT_SETUP =
+ "com.google.android.tv.action.LAUNCH_INPUT_SETUP";
+ /**
+ * A Google specific constant of the key for intent to launch actual TV input setup activity
+ * used with {@link #INTENT_ACTION_INPUT_SETUP}.
+ *
+ * <p>Value type: Intent (Parcelable)
+ *
+ * @deprecated Use {@link #EXTRA_SETUP_INTENT} instead
+ */
+ @Deprecated
+ private static final String EXTRA_GOOGLE_SETUP_INTENT =
+ "com.google.android.tv.extra.SETUP_INTENT";
+ /**
+ * A Google specific constant of the key to indicate an Activity launch intent for the intent
+ * action {@link #INTENT_ACTION_INPUT_SETUP}.
+ *
+ * <p>Value type: Intent (Parcelable)
+ *
+ * @deprecated Use {@link #EXTRA_ACTIVITY_AFTER_COMPLETION} instead
+ */
+ @Deprecated
+ private static final String EXTRA_GOOGLE_ACTIVITY_AFTER_COMPLETION =
+ "com.google.android.tv.intent.extra.ACTIVITY_AFTER_COMPLETION";
+
+ public static void removeSetupIntent(Bundle extras) {
+ extras.remove(EXTRA_SETUP_INTENT);
+ extras.remove(EXTRA_GOOGLE_SETUP_INTENT);
+ }
+
+ @Nullable
+ public static Intent getExtraSetupIntent(Intent intent) {
+ Bundle extras = intent.getExtras();
+ if (extras == null) {
+ return null;
+ }
+ Intent setupIntent = extras.getParcelable(EXTRA_SETUP_INTENT);
+ return setupIntent != null ? setupIntent : extras.getParcelable(EXTRA_GOOGLE_SETUP_INTENT);
+ }
+
+ @Nullable
+ public static Intent getExtraActivityAfter(Intent intent) {
+ Bundle extras = intent.getExtras();
+ if (extras == null) {
+ return null;
+ }
+ Intent setupIntent = extras.getParcelable(EXTRA_ACTIVITY_AFTER_COMPLETION);
+ return setupIntent != null
+ ? setupIntent
+ : extras.getParcelable(EXTRA_GOOGLE_ACTIVITY_AFTER_COMPLETION);
+ }
+
+ public static boolean hasInputSetupAction(Intent intent) {
+ String action = intent.getAction();
+ return INTENT_ACTION_INPUT_SETUP.equals(action)
+ || INTENT_GOOGLE_ACTION_INPUT_SETUP.equals(action);
+ }
+}
diff --git a/common/src/com/android/tv/common/annotation/UsedByNative.java b/common/src/com/android/tv/common/annotation/UsedByNative.java
new file mode 100644
index 00000000..dc829e08
--- /dev/null
+++ b/common/src/com/android/tv/common/annotation/UsedByNative.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.common.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation used for marking methods and fields that are called from native
+ * code. Useful for keeping components that would otherwise be removed by
+ * Proguard. Use the value parameter to mention a file that calls this method.
+ *
+ * Note that adding this annotation to a method is not enough to guarantee that
+ * it is kept - either its class must be referenced elsewhere in the program, or
+ * the class must be annotated with this as well.
+ *
+ * Usage example:<br />
+ * <pre>{@code
+ * @UsedByNative("NativeCrashHandler.cpp")
+ * public static void reportCrash(int signal, int code, int address) {
+ * ...
+ * }
+ * </pre>
+ *
+ */
+@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE, ElementType.CONSTRUCTOR})
+public @interface UsedByNative {
+ String value();
+}
diff --git a/common/src/com/android/tv/common/concurrent/NamedThreadFactory.java b/common/src/com/android/tv/common/concurrent/NamedThreadFactory.java
new file mode 100644
index 00000000..4c8371f3
--- /dev/null
+++ b/common/src/com/android/tv/common/concurrent/NamedThreadFactory.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.common.concurrent;
+
+import android.support.annotation.NonNull;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/** A thread factory that creates threads with named <code>prefix-##</code>. */
+public class NamedThreadFactory implements ThreadFactory {
+ private final AtomicInteger mCount = new AtomicInteger(0);
+ private final ThreadFactory mDefaultThreadFactory;
+ private final String mPrefix;
+
+ public NamedThreadFactory(final String prefix) {
+ mDefaultThreadFactory = Executors.defaultThreadFactory();
+ mPrefix = prefix + "-";
+ }
+
+ @Override
+ public Thread newThread(@NonNull final Runnable runnable) {
+ final Thread thread = mDefaultThreadFactory.newThread(runnable);
+ thread.setName(mPrefix + mCount.getAndIncrement());
+ return thread;
+ }
+
+ public boolean namedWithPrefix(Thread thread) {
+ return thread.getName().startsWith(mPrefix);
+ }
+}
diff --git a/common/src/com/android/tv/common/config/DefaultConfigManager.java b/common/src/com/android/tv/common/config/DefaultConfigManager.java
new file mode 100644
index 00000000..ae240855
--- /dev/null
+++ b/common/src/com/android/tv/common/config/DefaultConfigManager.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.common.config;
+
+import android.content.Context;
+import com.android.tv.common.config.api.RemoteConfig;
+
+/** Stub Remote Config. */
+public class DefaultConfigManager {
+ public static final long DEFAULT_LONG_VALUE = 0;
+
+ public static DefaultConfigManager createInstance(Context context) {
+ return new DefaultConfigManager();
+ }
+
+ private StubRemoteConfig mRemoteConfig = new StubRemoteConfig();
+
+ public RemoteConfig getRemoteConfig() {
+ return mRemoteConfig;
+ }
+
+ private static class StubRemoteConfig implements RemoteConfig {
+ @Override
+ public void fetch(OnRemoteConfigUpdatedListener listener) {}
+
+ @Override
+ public String getString(String key) {
+ return null;
+ }
+
+ @Override
+ public boolean getBoolean(String key) {
+ return false;
+ }
+
+ @Override
+ public long getLong(String key) {
+ return DEFAULT_LONG_VALUE;
+ }
+
+ @Override
+ public long getLong(String key, long defaultValue) {
+ return defaultValue;
+ }
+ }
+}
diff --git a/common/src/com/android/tv/common/config/RemoteConfigFeature.java b/common/src/com/android/tv/common/config/RemoteConfigFeature.java
new file mode 100644
index 00000000..2ea381f0
--- /dev/null
+++ b/common/src/com/android/tv/common/config/RemoteConfigFeature.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.common.config;
+
+import android.content.Context;
+import com.android.tv.common.BaseApplication;
+import com.android.tv.common.feature.Feature;
+
+/**
+ * A {@link Feature} controlled by a {@link com.android.tv.common.config.api.RemoteConfig} boolean.
+ */
+public class RemoteConfigFeature implements Feature {
+ private final String mKey;
+
+ /** Creates a {@link RemoteConfigFeature for the {@code key}. */
+ public static RemoteConfigFeature fromKey(String key) {
+ return new RemoteConfigFeature(key);
+ }
+
+ private RemoteConfigFeature(String key) {
+ mKey = key;
+ }
+
+ @Override
+ public boolean isEnabled(Context context) {
+ return BaseApplication.getSingletons(context).getRemoteConfig().getBoolean(mKey);
+ }
+}
diff --git a/common/src/com/android/tv/common/config/api/RemoteConfig.java b/common/src/com/android/tv/common/config/api/RemoteConfig.java
new file mode 100644
index 00000000..74597f9d
--- /dev/null
+++ b/common/src/com/android/tv/common/config/api/RemoteConfig.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.common.config.api;
+
+/**
+ * Manages Live TV Configuration, allowing remote updates.
+ *
+ * <p>This is a thin wrapper around <a
+ * href="https://firebase.google.com/docs/remote-config/"></a>Firebase Remote Config</a>
+ */
+public interface RemoteConfig {
+
+ /** Used to inject a remote config */
+ interface HasRemoteConfig {
+ RemoteConfig getRemoteConfig();
+ }
+
+ /** Notified on successful completion of a {@link #fetch)} */
+ interface OnRemoteConfigUpdatedListener {
+ void onRemoteConfigUpdated();
+ }
+
+ /** Starts a fetch and notifies {@code listener} on successful completion. */
+ void fetch(OnRemoteConfigUpdatedListener listener);
+
+ /** Gets value as a string corresponding to the specified key. */
+ String getString(String key);
+
+ /** Gets value as a boolean corresponding to the specified key. */
+ boolean getBoolean(String key);
+
+ /** Gets value as a long corresponding to the specified key. */
+ long getLong(String key);
+
+ /**
+ * Gets value as a long corresponding to the specified key. Returns the defaultValue if no value
+ * is found.
+ */
+ long getLong(String key, long defaultValue);
+}
diff --git a/common/src/com/android/tv/common/config/api/RemoteConfigValue.java b/common/src/com/android/tv/common/config/api/RemoteConfigValue.java
new file mode 100644
index 00000000..6da89fb9
--- /dev/null
+++ b/common/src/com/android/tv/common/config/api/RemoteConfigValue.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.config.api;
+
+/** Wrapper for a RemoteConfig key and default value. */
+public abstract class RemoteConfigValue<T> {
+ private final T defaultValue;
+ private final String key;
+
+ private RemoteConfigValue(String key, T defaultValue) {
+ this.defaultValue = defaultValue;
+ this.key = key;
+ }
+
+ /** Create with the given key and default value */
+ public static RemoteConfigValue<Long> create(String key, long defaultValue) {
+ return new RemoteConfigValue<Long>(key, defaultValue) {
+ @Override
+ public Long get(RemoteConfig remoteConfig) {
+ return remoteConfig.getLong(key, defaultValue);
+ }
+ };
+ }
+
+ public abstract T get(RemoteConfig remoteConfig);
+
+ public final T getDefaultValue() {
+ return defaultValue;
+ }
+
+ @Override
+ public final String toString() {
+ return "RemoteConfigValue(key=" + key + ", defalutValue=" + defaultValue + "]";
+ }
+}
diff --git a/common/src/com/android/tv/common/customization/CustomAction.java b/common/src/com/android/tv/common/customization/CustomAction.java
new file mode 100644
index 00000000..ee18cb3c
--- /dev/null
+++ b/common/src/com/android/tv/common/customization/CustomAction.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.common.customization;
+
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+
+/** Describes a custom option defined in customization package. This will be added to main menu. */
+public class CustomAction implements Comparable<CustomAction> {
+ private static final int POSITION_THRESHOLD = 100;
+
+ private final int mPositionPriority;
+ private final String mTitle;
+ private final Drawable mIconDrawable;
+ private final Intent mIntent;
+
+ public CustomAction(int positionPriority, String title, Drawable iconDrawable, Intent intent) {
+ mPositionPriority = positionPriority;
+ mTitle = title;
+ mIconDrawable = iconDrawable;
+ mIntent = intent;
+ }
+
+ /**
+ * Returns if this option comes before the existing items. Note that custom options can only be
+ * placed at the front or back. (i.e. cannot be added in the middle of existing options.)
+ *
+ * @return {@code true} if it goes to the beginning. {@code false} if it goes to the end.
+ */
+ public boolean isFront() {
+ return mPositionPriority < POSITION_THRESHOLD;
+ }
+
+ @Override
+ public int compareTo(@NonNull CustomAction another) {
+ return mPositionPriority - another.mPositionPriority;
+ }
+
+ /** Returns title. */
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /** Returns icon drawable. */
+ public Drawable getIconDrawable() {
+ return mIconDrawable;
+ }
+
+ /** Returns intent to launch when this option is clicked. */
+ public Intent getIntent() {
+ return mIntent;
+ }
+}
diff --git a/common/src/com/android/tv/common/customization/CustomizationManager.java b/common/src/com/android/tv/common/customization/CustomizationManager.java
new file mode 100644
index 00000000..09ecaef8
--- /dev/null
+++ b/common/src/com/android/tv/common/customization/CustomizationManager.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.common.customization;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.IntDef;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.tv.common.CommonConstants;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class CustomizationManager {
+ private static final String TAG = "CustomizationManager";
+ private static final boolean DEBUG = false;
+
+ private static final String[] CUSTOMIZE_PERMISSIONS = {
+ CommonConstants.BASE_PACKAGE + ".permission.CUSTOMIZE_TV_APP"
+ };
+
+ private static final String CATEGORY_TV_CUSTOMIZATION =
+ CommonConstants.BASE_PACKAGE + ".category";
+
+ /** Row IDs to share customized actions. Only rows listed below can have customized action. */
+ public static final String ID_OPTIONS_ROW = "options_row";
+
+ public static final String ID_PARTNER_ROW = "partner_row";
+
+ @IntDef({TRICKPLAY_MODE_ENABLED, TRICKPLAY_MODE_DISABLED, TRICKPLAY_MODE_USE_EXTERNAL_STORAGE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TRICKPLAY_MODE {}
+
+ public static final int TRICKPLAY_MODE_ENABLED = 0;
+ public static final int TRICKPLAY_MODE_DISABLED = 1;
+ public static final int TRICKPLAY_MODE_USE_EXTERNAL_STORAGE = 2;
+
+ private static final String[] TRICKPLAY_MODE_STRINGS = {
+ "enabled", "disabled", "use_external_storage_only"
+ };
+
+ private static final HashMap<String, String> INTENT_CATEGORY_TO_ROW_ID;
+
+ static {
+ INTENT_CATEGORY_TO_ROW_ID = new HashMap<>();
+ INTENT_CATEGORY_TO_ROW_ID.put(CATEGORY_TV_CUSTOMIZATION + ".OPTIONS_ROW", ID_OPTIONS_ROW);
+ INTENT_CATEGORY_TO_ROW_ID.put(CATEGORY_TV_CUSTOMIZATION + ".PARTNER_ROW", ID_PARTNER_ROW);
+ }
+
+ private static final String RES_ID_PARTNER_ROW_TITLE = "partner_row_title";
+ private static final String RES_ID_HAS_LINUX_DVB_BUILT_IN_TUNER =
+ "has_linux_dvb_built_in_tuner";
+ private static final String RES_ID_TRICKPLAY_MODE = "trickplay_mode";
+
+ private static final String RES_TYPE_STRING = "string";
+ private static final String RES_TYPE_BOOLEAN = "bool";
+
+ private static String sCustomizationPackage;
+ private static Boolean sHasLinuxDvbBuiltInTuner;
+ private static @TRICKPLAY_MODE Integer sTrickplayMode;
+
+ private final Context mContext;
+ private boolean mInitialized;
+
+ private String mPartnerRowTitle;
+ private final Map<String, List<CustomAction>> mRowIdToCustomActionsMap = new HashMap<>();
+
+ public CustomizationManager(Context context) {
+ mContext = context;
+ mInitialized = false;
+ }
+
+ /**
+ * Returns {@code true} if there's a customization package installed and it specifies built-in
+ * tuner devices are available. The built-in tuner should support DVB API to be recognized by
+ * Live TV.
+ */
+ public static boolean hasLinuxDvbBuiltInTuner(Context context) {
+ if (sHasLinuxDvbBuiltInTuner == null) {
+ if (TextUtils.isEmpty(getCustomizationPackageName(context))) {
+ sHasLinuxDvbBuiltInTuner = false;
+ } else {
+ try {
+ Resources res =
+ context.getPackageManager()
+ .getResourcesForApplication(sCustomizationPackage);
+ int resId =
+ res.getIdentifier(
+ RES_ID_HAS_LINUX_DVB_BUILT_IN_TUNER,
+ RES_TYPE_BOOLEAN,
+ sCustomizationPackage);
+ sHasLinuxDvbBuiltInTuner = resId != 0 && res.getBoolean(resId);
+ } catch (NameNotFoundException e) {
+ sHasLinuxDvbBuiltInTuner = false;
+ }
+ }
+ }
+ return sHasLinuxDvbBuiltInTuner;
+ }
+
+ public static @TRICKPLAY_MODE int getTrickplayMode(Context context) {
+ if (sTrickplayMode == null) {
+ if (TextUtils.isEmpty(getCustomizationPackageName(context))) {
+ sTrickplayMode = TRICKPLAY_MODE_ENABLED;
+ } else {
+ try {
+ String customization = null;
+ Resources res =
+ context.getPackageManager()
+ .getResourcesForApplication(sCustomizationPackage);
+ int resId =
+ res.getIdentifier(
+ RES_ID_TRICKPLAY_MODE, RES_TYPE_STRING, sCustomizationPackage);
+ customization = resId == 0 ? null : res.getString(resId);
+ sTrickplayMode = TRICKPLAY_MODE_ENABLED;
+ if (customization != null) {
+ for (int i = 0; i < TRICKPLAY_MODE_STRINGS.length; ++i) {
+ if (TRICKPLAY_MODE_STRINGS[i].equalsIgnoreCase(customization)) {
+ sTrickplayMode = i;
+ break;
+ }
+ }
+ }
+ } catch (NameNotFoundException e) {
+ sTrickplayMode = TRICKPLAY_MODE_ENABLED;
+ }
+ }
+ }
+ return sTrickplayMode;
+ }
+
+ private static String getCustomizationPackageName(Context context) {
+ if (sCustomizationPackage == null) {
+ List<PackageInfo> packageInfos =
+ context.getPackageManager()
+ .getPackagesHoldingPermissions(CUSTOMIZE_PERMISSIONS, 0);
+ sCustomizationPackage = packageInfos.size() == 0 ? "" : packageInfos.get(0).packageName;
+ }
+ return sCustomizationPackage;
+ }
+
+ /** Initialize TV customization options. Run this API only on the main thread. */
+ public void initialize() {
+ if (mInitialized) {
+ return;
+ }
+ mInitialized = true;
+ if (!TextUtils.isEmpty(getCustomizationPackageName(mContext))) {
+ buildCustomActions();
+ buildPartnerRow();
+ }
+ }
+
+ private void buildCustomActions() {
+ mRowIdToCustomActionsMap.clear();
+ PackageManager pm = mContext.getPackageManager();
+ for (String intentCategory : INTENT_CATEGORY_TO_ROW_ID.keySet()) {
+ Intent customOptionIntent = new Intent(Intent.ACTION_MAIN);
+ customOptionIntent.addCategory(intentCategory);
+
+ List<ResolveInfo> activities =
+ pm.queryIntentActivities(
+ customOptionIntent,
+ PackageManager.GET_RECEIVERS
+ | PackageManager.GET_RESOLVED_FILTER
+ | PackageManager.GET_META_DATA);
+ for (ResolveInfo info : activities) {
+ String packageName = info.activityInfo.packageName;
+ if (!TextUtils.equals(packageName, sCustomizationPackage)) {
+ Log.w(
+ TAG,
+ "A customization package "
+ + sCustomizationPackage
+ + " already exist. Ignoring "
+ + packageName);
+ continue;
+ }
+
+ int position = info.filter.getPriority();
+ String title = info.loadLabel(pm).toString();
+ Drawable drawable = info.loadIcon(pm);
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.addCategory(intentCategory);
+ intent.setClassName(sCustomizationPackage, info.activityInfo.name);
+
+ String rowId = INTENT_CATEGORY_TO_ROW_ID.get(intentCategory);
+ List<CustomAction> actions = mRowIdToCustomActionsMap.get(rowId);
+ if (actions == null) {
+ actions = new ArrayList<>();
+ mRowIdToCustomActionsMap.put(rowId, actions);
+ }
+ actions.add(new CustomAction(position, title, drawable, intent));
+ }
+ }
+ // Sort items by position
+ for (List<CustomAction> actions : mRowIdToCustomActionsMap.values()) {
+ Collections.sort(actions);
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "Dumping custom actions");
+ for (String id : mRowIdToCustomActionsMap.keySet()) {
+ for (CustomAction action : mRowIdToCustomActionsMap.get(id)) {
+ Log.d(
+ TAG,
+ "Custom row rowId="
+ + id
+ + " title="
+ + action.getTitle()
+ + " class="
+ + action.getIntent());
+ }
+ }
+ Log.d(TAG, "Dumping custom actions - end of dump");
+ }
+ }
+
+ /**
+ * Returns custom actions for given row id.
+ *
+ * <p>Row ID is one of ID_OPTIONS_ROW or ID_PARTNER_ROW.
+ */
+ public List<CustomAction> getCustomActions(String rowId) {
+ return mRowIdToCustomActionsMap.get(rowId);
+ }
+
+ private void buildPartnerRow() {
+ mPartnerRowTitle = null;
+ Resources res;
+ try {
+ res = mContext.getPackageManager().getResourcesForApplication(sCustomizationPackage);
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "Could not get resources for package " + sCustomizationPackage);
+ return;
+ }
+ int resId =
+ res.getIdentifier(RES_ID_PARTNER_ROW_TITLE, RES_TYPE_STRING, sCustomizationPackage);
+ if (resId != 0) {
+ mPartnerRowTitle = res.getString(resId);
+ }
+ if (DEBUG) Log.d(TAG, "Partner row title [" + mPartnerRowTitle + "]");
+ }
+
+ /** Returns partner row title. */
+ public String getPartnerRowTitle() {
+ return mPartnerRowTitle;
+ }
+}
diff --git a/common/src/com/android/tv/common/experiments/ExperimentFlag.java b/common/src/com/android/tv/common/experiments/ExperimentFlag.java
new file mode 100644
index 00000000..c9bacac4
--- /dev/null
+++ b/common/src/com/android/tv/common/experiments/ExperimentFlag.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.common.experiments;
+
+import android.support.annotation.VisibleForTesting;
+import com.android.tv.common.BuildConfig;
+
+
+/** Experiments return values based on user, device and other criteria. */
+public final class ExperimentFlag<T> {
+
+ private static boolean sAllowOverrides = false;
+
+ @VisibleForTesting
+ public static void initForTest() {
+ sAllowOverrides = true;
+ }
+
+ /** Returns a boolean experiment */
+ public static ExperimentFlag<Boolean> createFlag(
+ boolean defaultValue) {
+ return new ExperimentFlag<>(
+ defaultValue);
+ }
+
+ private final T mDefaultValue;
+
+ private T mOverrideValue = null;
+ private boolean mOverridden = false;
+
+ private ExperimentFlag(
+ T defaultValue) {
+ mDefaultValue = defaultValue;
+ }
+
+ /** Returns value for this experiment */
+ public T get() {
+ return sAllowOverrides && mOverridden ? mOverrideValue : mDefaultValue;
+ }
+
+ @VisibleForTesting
+ public void override(T t) {
+ if (sAllowOverrides) {
+ mOverridden = true;
+ mOverrideValue = t;
+ }
+ }
+
+ @VisibleForTesting
+ public void resetOverride() {
+ mOverridden = false;
+ }
+}
diff --git a/common/src/com/android/tv/common/annotation/UsedByReflection.java b/common/src/com/android/tv/common/experiments/ExperimentLoader.java
index 5a4517f7..5f012e11 100644
--- a/common/src/com/android/tv/common/annotation/UsedByReflection.java
+++ b/common/src/com/android/tv/common/experiments/ExperimentLoader.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,16 +14,15 @@
* limitations under the License.
*/
-package com.android.tv.common.annotation;
+package com.android.tv.common.experiments;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
+import android.content.Context;
-import java.lang.annotation.Retention;
+/** Used to sync {@link ExperimentFlag}s. */
+public class ExperimentLoader {
-/**
- * Denotes that the method or field is used by reflection even though it is not ever called
- * directly.
- */
-@Retention(SOURCE)
-public @interface UsedByReflection {
+ /** Starts a background task to update {@link ExperimentFlag}s */
+ public void asyncRefreshExperiments(Context context) {
+ // Override for your experiment system
+ }
}
diff --git a/common/src/com/android/tv/common/experiments/Experiments.java b/common/src/com/android/tv/common/experiments/Experiments.java
new file mode 100644
index 00000000..96b15e53
--- /dev/null
+++ b/common/src/com/android/tv/common/experiments/Experiments.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.common.experiments;
+
+import static com.android.tv.common.experiments.ExperimentFlag.createFlag;
+
+import com.android.tv.common.BuildConfig;
+
+/**
+ * Set of experiments visible in AOSP.
+ *
+ * <p>This file is maintained by hand.
+ */
+public final class Experiments {
+ public static final ExperimentFlag<Boolean> CLOUD_EPG =
+ ExperimentFlag.createFlag(
+ true);
+
+ public static final ExperimentFlag<Boolean> ENABLE_UNRATED_CONTENT_SETTINGS =
+ ExperimentFlag.createFlag(
+ false);
+
+ /** Turn analytics on or off based on the System Checkbox for logging. */
+ public static final ExperimentFlag<Boolean> ENABLE_ANALYTICS_VIA_CHECKBOX =
+ createFlag(
+ false);
+
+ /**
+ * Allow developer features such as the dev menu and other aids.
+ *
+ * <p>These features are available to select users(aka fishfooders) on production builds.
+ */
+ public static final ExperimentFlag<Boolean> ENABLE_DEVELOPER_FEATURES =
+ ExperimentFlag.createFlag(
+ BuildConfig.ENG);
+
+ /**
+ * Allow QA features.
+ *
+ * <p>These features must be carefully limited, keeping QA differences to a minimum.
+ *
+ * <p>These features are available to select users(aka QA) on production builds.
+ */
+ public static final ExperimentFlag<Boolean> ENABLE_QA_FEATURES =
+ ExperimentFlag.createFlag(
+ false);
+
+ private Experiments() {}
+}
diff --git a/common/src/com/android/tv/common/feature/CommonFeatures.java b/common/src/com/android/tv/common/feature/CommonFeatures.java
index 62c88ead..1fceabb3 100644
--- a/common/src/com/android/tv/common/feature/CommonFeatures.java
+++ b/common/src/com/android/tv/common/feature/CommonFeatures.java
@@ -19,36 +19,81 @@ package com.android.tv.common.feature;
import static com.android.tv.common.feature.FeatureUtils.AND;
import static com.android.tv.common.feature.TestableFeature.createTestableFeature;
+import android.content.Context;
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.tv.common.config.api.RemoteConfig.HasRemoteConfig;
+import com.android.tv.common.experiments.Experiments;
+
+import com.android.tv.common.util.CommonUtils;
+import com.android.tv.common.util.LocationUtils;
+
/**
* List of {@link Feature} that affect more than just the Live TV app.
*
* <p>Remove the {@code Feature} once it is launched.
*/
public class CommonFeatures {
+ private static final String TAG = "CommonFeatures";
+ private static final boolean DEBUG = false;
+
/**
* DVR
*
* <p>See <a href="https://goto.google.com/atv-dvr-onepager">go/atv-dvr-onepager</a>
*
- * DVR API is introduced in N, it only works when app runs as a system app.
+ * <p>DVR API is introduced in N, it only works when app runs as a system app.
*/
- public static final TestableFeature DVR = createTestableFeature(
- AND(Sdk.AT_LEAST_N, SystemAppFeature.SYSTEM_APP_FEATURE));
+ public static final TestableFeature DVR =
+ createTestableFeature(AND(Sdk.AT_LEAST_N, SystemAppFeature.SYSTEM_APP_FEATURE));
/**
* ENABLE_RECORDING_REGARDLESS_OF_STORAGE_STATUS
*
- * Enables dvr recording regardless of storage status.
+ * <p>Enables dvr recording regardless of storage status.
*/
public static final Feature FORCE_RECORDING_UNTIL_NO_SPACE =
- new PropertyFeature("force_recording_until_no_space", false);
+ PropertyFeature.create("force_recording_until_no_space", false);
- /**
- * USE_SW_CODEC_FOR_SD
- *
- * Prefer software based codec for SD channels.
- */
- public static final Feature USE_SW_CODEC_FOR_SD =
- new PropertyFeature("use_sw_codec_for_sd", false
- );
+ public static final Feature TUNER =
+ new Feature() {
+ @Override
+ public boolean isEnabled(Context context) {
+
+ if (CommonUtils.isDeveloper()) {
+ // we enable tuner for developers to test tuner in any platform.
+ return true;
+ }
+
+ // This is special handling just for USB Tuner.
+ // It does not require any N API's but relies on a improvements in N for AC3
+ // support
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
+ }
+ };
+
+ /** Show postal code fragment before channel scan. */
+ public static final Feature ENABLE_CLOUD_EPG_REGION =
+ new Feature() {
+ private final String[] supportedRegions = {
+ };
+
+
+ @Override
+ public boolean isEnabled(Context context) {
+ if (!Experiments.CLOUD_EPG.get()) {
+ if (DEBUG) Log.d(TAG, "Experiments.CLOUD_EPG is false");
+ return false;
+ }
+ String country = LocationUtils.getCurrentCountry(context);
+ for (int i = 0; i < supportedRegions.length; i++) {
+ if (supportedRegions[i].equalsIgnoreCase(country)) {
+ return true;
+ }
+ }
+ if (DEBUG) Log.d(TAG, "EPG flag false after country check");
+ return false;
+ }
+ };
}
diff --git a/common/src/com/android/tv/common/feature/EngOnlyFeature.java b/common/src/com/android/tv/common/feature/EngOnlyFeature.java
index 9fc39d9f..5feb5481 100644
--- a/common/src/com/android/tv/common/feature/EngOnlyFeature.java
+++ b/common/src/com/android/tv/common/feature/EngOnlyFeature.java
@@ -17,16 +17,13 @@
package com.android.tv.common.feature;
import android.content.Context;
-
import com.android.tv.common.BuildConfig;
-/**
- * A feature that is only available on {@link BuildConfig#ENG} builds.
- */
+/** A feature that is only available on {@link BuildConfig#ENG} builds. */
public final class EngOnlyFeature implements Feature {
public static final Feature ENG_ONLY_FEATURE = new EngOnlyFeature();
- private EngOnlyFeature() { }
+ private EngOnlyFeature() {}
@Override
public boolean isEnabled(Context context) {
diff --git a/common/src/com/android/tv/common/feature/ExperimentFeature.java b/common/src/com/android/tv/common/feature/ExperimentFeature.java
new file mode 100644
index 00000000..820eda49
--- /dev/null
+++ b/common/src/com/android/tv/common/feature/ExperimentFeature.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.common.feature;
+
+import android.content.Context;
+import com.android.tv.common.experiments.ExperimentFlag;
+
+/** A {@link Feature} base on an {@link ExperimentFlag}. */
+public final class ExperimentFeature implements Feature {
+
+ public static Feature from(ExperimentFlag<Boolean> flag) {
+ return new ExperimentFeature(flag);
+ }
+
+ private final ExperimentFlag<Boolean> mFlag;
+
+ private ExperimentFeature(ExperimentFlag<Boolean> flag) {
+ mFlag = flag;
+ }
+
+ @Override
+ public boolean isEnabled(Context context) {
+ return mFlag.get();
+ }
+
+ @Override
+ public String toString() {
+ return "ExperimentFeature for " + mFlag;
+ }
+}
diff --git a/common/src/com/android/tv/common/feature/Feature.java b/common/src/com/android/tv/common/feature/Feature.java
index b3a8336c..fd5625e5 100644
--- a/common/src/com/android/tv/common/feature/Feature.java
+++ b/common/src/com/android/tv/common/feature/Feature.java
@@ -23,16 +23,15 @@ import android.content.Context;
* launched.
*
* <p>Expected usage is:
+ *
* <pre>{@code
- * if (MY_FEATURE.isEnabled(context) {
- * showNewCoolUi();
- * } else{
- * showOldBoringUi();
- * }
+ * if (MY_FEATURE.isEnabled(context) {
+ * showNewCoolUi();
+ * } else{
+ * showOldBoringUi();
+ * }
* }</pre>
*/
public interface Feature {
boolean isEnabled(Context context);
-
-
}
diff --git a/common/src/com/android/tv/common/feature/FeatureUtils.java b/common/src/com/android/tv/common/feature/FeatureUtils.java
index f60b2048..8650d151 100644
--- a/common/src/com/android/tv/common/feature/FeatureUtils.java
+++ b/common/src/com/android/tv/common/feature/FeatureUtils.java
@@ -17,12 +17,10 @@
package com.android.tv.common.feature;
import android.content.Context;
-
+import com.android.tv.common.util.CommonUtils;
import java.util.Arrays;
-/**
- * Static utilities for features.
- */
+/** Static utilities for features. */
public class FeatureUtils {
/**
@@ -47,7 +45,6 @@ public class FeatureUtils {
return "or(" + Arrays.asList(features) + ")";
}
};
-
}
/**
@@ -74,36 +71,47 @@ public class FeatureUtils {
};
}
- /**
- * A feature that is always enabled.
- */
- public static final Feature ON = new Feature() {
- @Override
- public boolean isEnabled(Context context) {
- return true;
- }
+ /** A feature that is always enabled. */
+ public static final Feature ON =
+ new Feature() {
+ @Override
+ public boolean isEnabled(Context context) {
+ return true;
+ }
- @Override
- public String toString() {
- return "on";
- }
- };
+ @Override
+ public String toString() {
+ return "on";
+ }
+ };
- /**
- * A feature that is always disabled.
- */
- public static final Feature OFF = new Feature() {
- @Override
- public boolean isEnabled(Context context) {
- return false;
- }
+ /** A feature that is always disabled. */
+ public static final Feature OFF =
+ new Feature() {
+ @Override
+ public boolean isEnabled(Context context) {
+ return false;
+ }
- @Override
- public String toString() {
- return "off";
- }
- };
+ @Override
+ public String toString() {
+ return "off";
+ }
+ };
- private FeatureUtils() {
- }
+ /** True if running in robolectric. */
+ public static final Feature ROBOLECTRIC =
+ new Feature() {
+ @Override
+ public boolean isEnabled(Context context) {
+ return CommonUtils.isRoboTest();
+ }
+
+ @Override
+ public String toString() {
+ return "isRobolecteric";
+ }
+ };
+
+ private FeatureUtils() {}
}
diff --git a/common/src/com/android/tv/common/feature/GServiceFeature.java b/common/src/com/android/tv/common/feature/GServiceFeature.java
index 9e6e11a6..1d7d1156 100644
--- a/common/src/com/android/tv/common/feature/GServiceFeature.java
+++ b/common/src/com/android/tv/common/feature/GServiceFeature.java
@@ -18,10 +18,7 @@ package com.android.tv.common.feature;
import android.content.Context;
-
-/**
- * A feature controlled by a GServices flag.
- */
+/** A feature controlled by a GServices flag. */
public class GServiceFeature implements Feature {
private static final String LIVECHANNELS_PREFIX = "livechannels:";
private final String mKey;
diff --git a/common/src/com/android/tv/common/feature/Model.java b/common/src/com/android/tv/common/feature/Model.java
new file mode 100644
index 00000000..7aa5148e
--- /dev/null
+++ b/common/src/com/android/tv/common/feature/Model.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.common.feature;
+
+import android.content.Context;
+
+/** Holder for {@link android.os.Build#MODEL} features. */
+public interface Model {
+
+ ModelFeature NEXUS_PLAYER = new ModelFeature("Nexus Player");
+
+ /** True when the {@link android.os.Build#MODEL} equals the {@code model} given. */
+ public static final class ModelFeature implements Feature {
+ private final String mModel;
+
+ private ModelFeature(String model) {
+ mModel = model;
+ }
+
+ @Override
+ public boolean isEnabled(Context context) {
+ return isEnabled();
+ }
+
+ public boolean isEnabled() {
+ return android.os.Build.MODEL.equals(mModel);
+ }
+
+ @Override
+ public String toString() {
+ return "ModelFeature(" + mModel + ")=" + isEnabled();
+ }
+ }
+}
diff --git a/common/src/com/android/tv/common/feature/PackageVersionFeature.java b/common/src/com/android/tv/common/feature/PackageVersionFeature.java
index 7f615d31..f41a14a0 100644
--- a/common/src/com/android/tv/common/feature/PackageVersionFeature.java
+++ b/common/src/com/android/tv/common/feature/PackageVersionFeature.java
@@ -49,7 +49,10 @@ public class PackageVersionFeature implements Feature {
@Override
public String toString() {
- return "PackageVersionFeature[packageName=" + mPackageName + ",requiredVersion="
- + mRequiredVersionCode + "]";
+ return "PackageVersionFeature[packageName="
+ + mPackageName
+ + ",requiredVersion="
+ + mRequiredVersionCode
+ + "]";
}
}
diff --git a/common/src/com/android/tv/common/feature/PropertyFeature.java b/common/src/com/android/tv/common/feature/PropertyFeature.java
index fdcffa04..0cf36317 100644
--- a/common/src/com/android/tv/common/feature/PropertyFeature.java
+++ b/common/src/com/android/tv/common/feature/PropertyFeature.java
@@ -17,7 +17,6 @@
package com.android.tv.common.feature;
import android.content.Context;
-
import com.android.tv.common.BooleanSystemProperty;
/**
@@ -26,21 +25,29 @@ import com.android.tv.common.BooleanSystemProperty;
* <p>See {@link BooleanSystemProperty} for instructions on how to set using adb.
*/
public final class PropertyFeature implements Feature {
+
+ public static PropertyFeature create(String key, boolean defaultValue) {
+ return new PropertyFeature(key, defaultValue);
+ }
+
private final BooleanSystemProperty mProperty;
/**
* Create System Property backed feature.
*
- * @param key the system property key. Length must be <= 31 characters.
+ * @param key the system property key. Length must be <= 31 characters.
* @param defaultValue the value to return if the property is undefined or empty.
*/
- public PropertyFeature(String key, boolean defaultValue) {
+ private PropertyFeature(String key, boolean defaultValue) {
if (key.length() > 31) {
// Since Features are initialized at startup and the keys are static go ahead and kill
// the application.
throw new IllegalArgumentException(
- "Property keys have a max length of 31 characters but '" + key + "' is " + key
- .length() + " characters.");
+ "Property keys have a max length of 31 characters but '"
+ + key
+ + "' is "
+ + key.length()
+ + " characters.");
}
mProperty = new BooleanSystemProperty(key, defaultValue);
}
diff --git a/common/src/com/android/tv/common/feature/Sdk.java b/common/src/com/android/tv/common/feature/Sdk.java
index 9f99a64f..155b391d 100644
--- a/common/src/com/android/tv/common/feature/Sdk.java
+++ b/common/src/com/android/tv/common/feature/Sdk.java
@@ -19,10 +19,8 @@ package com.android.tv.common.feature;
import android.content.Context;
import android.os.Build;
-/**
- * Holder for SDK version features
- */
-public class Sdk {
+/** Holder for SDK version features */
+public final class Sdk {
public static final Feature AT_LEAST_N =
new Feature() {
@Override
@@ -31,5 +29,13 @@ public class Sdk {
}
};
+ public static final Feature AT_LEAST_O =
+ new Feature() {
+ @Override
+ public boolean isEnabled(Context context) {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
+ }
+ };
+
private Sdk() {}
}
diff --git a/common/src/com/android/tv/common/feature/SharedPreferencesFeature.java b/common/src/com/android/tv/common/feature/SharedPreferencesFeature.java
index 881f53d6..ef2260d6 100644
--- a/common/src/com/android/tv/common/feature/SharedPreferencesFeature.java
+++ b/common/src/com/android/tv/common/feature/SharedPreferencesFeature.java
@@ -19,11 +19,9 @@ package com.android.tv.common.feature;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
-import com.android.tv.common.SharedPreferencesUtils;
+import com.android.tv.common.util.SharedPreferencesUtils;
-/**
- * Feature controlled by shared preferences.
- */
+/** Feature controlled by shared preferences. */
public final class SharedPreferencesFeature implements Feature {
private static final String TAG = "SharedPrefFeature";
private static final boolean DEBUG = false;
@@ -53,8 +51,9 @@ public final class SharedPreferencesFeature implements Feature {
return false;
}
if (mSharedPreferences == null) {
- mSharedPreferences = context.getSharedPreferences(
- SharedPreferencesUtils.SHARED_PREF_FEATURES, Context.MODE_PRIVATE);
+ mSharedPreferences =
+ context.getSharedPreferences(
+ SharedPreferencesUtils.SHARED_PREF_FEATURES, Context.MODE_PRIVATE);
mEnabled = mSharedPreferences.getBoolean(mKey, mDefaultValue);
}
if (DEBUG) Log.d(TAG, mKey + " is " + mEnabled);
@@ -69,14 +68,14 @@ public final class SharedPreferencesFeature implements Feature {
public void setEnabled(Context context, boolean enable) {
if (DEBUG) Log.d(TAG, mKey + " is set to " + enable);
if (mSharedPreferences == null) {
- mSharedPreferences = context.getSharedPreferences(
- SharedPreferencesUtils.SHARED_PREF_FEATURES, Context.MODE_PRIVATE);
+ mSharedPreferences =
+ context.getSharedPreferences(
+ SharedPreferencesUtils.SHARED_PREF_FEATURES, Context.MODE_PRIVATE);
mEnabled = enable;
mSharedPreferences.edit().putBoolean(mKey, enable).apply();
} else if (mEnabled != enable) {
mEnabled = enable;
mSharedPreferences.edit().putBoolean(mKey, enable).apply();
}
-
}
}
diff --git a/common/src/com/android/tv/common/feature/SystemAppFeature.java b/common/src/com/android/tv/common/feature/SystemAppFeature.java
index 79fd32f3..1dd3b8dd 100644
--- a/common/src/com/android/tv/common/feature/SystemAppFeature.java
+++ b/common/src/com/android/tv/common/feature/SystemAppFeature.java
@@ -19,13 +19,11 @@ package com.android.tv.common.feature;
import android.content.Context;
import android.content.pm.ApplicationInfo;
-/**
- * A feature that is for system App.
- */
+/** A feature that is for system App. */
public final class SystemAppFeature implements Feature {
public static final Feature SYSTEM_APP_FEATURE = new SystemAppFeature();
- private SystemAppFeature() { }
+ private SystemAppFeature() {}
@Override
public boolean isEnabled(Context context) {
diff --git a/common/src/com/android/tv/common/feature/TestableFeature.java b/common/src/com/android/tv/common/feature/TestableFeature.java
index d7e707a1..1f18639d 100644
--- a/common/src/com/android/tv/common/feature/TestableFeature.java
+++ b/common/src/com/android/tv/common/feature/TestableFeature.java
@@ -19,33 +19,28 @@ package com.android.tv.common.feature;
import android.content.Context;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
-
-import com.android.tv.common.TvCommonUtils;
+import com.android.tv.common.util.CommonUtils;
/**
* When run in a test harness this feature can be turned on or off, overriding the normal value.
*
- * <p><b>Warning</b> making a feature testable will cause the code to stay in the APK and
- * could leak unreleased features.
+ * <p><b>Warning</b> making a feature testable will cause the code to stay in the APK and could leak
+ * unreleased features.
*/
public class TestableFeature implements Feature {
- private final static String TAG = "TestableFeature";
- private final static String DETAIL_MESSAGE
- = "TestableFeatures should only be changed in tests.";
+ private static final String TAG = "TestableFeature";
+ private static final String DETAIL_MESSAGE =
+ "TestableFeatures should only be changed in tests.";
private final Feature mDelegate;
private Boolean mTestValue = null;
- /**
- * Creates testable feature.
- */
+ /** Creates testable feature. */
public static TestableFeature createTestableFeature(Feature delegate) {
return new TestableFeature(delegate);
}
- /**
- * Creates testable feature with initial value.
- */
+ /** Creates testable feature with initial value. */
public static TestableFeature createTestableFeature(Feature delegate, Boolean initialValue) {
return new TestableFeature(delegate, initialValue);
}
@@ -61,9 +56,8 @@ public class TestableFeature implements Feature {
@VisibleForTesting
public void enableForTest() {
- if (!TvCommonUtils.isRunningInTest()) {
- Log.e(TAG, "Not enabling for test:" + this,
- new IllegalStateException(DETAIL_MESSAGE));
+ if (!CommonUtils.isRunningInTest()) {
+ Log.e(TAG, "Not enabling for test:" + this, new IllegalStateException(DETAIL_MESSAGE));
} else {
mTestValue = true;
}
@@ -71,8 +65,10 @@ public class TestableFeature implements Feature {
@VisibleForTesting
public void disableForTests() {
- if (!TvCommonUtils.isRunningInTest()) {
- Log.e(TAG, "Not disabling for test: " + this,
+ if (!CommonUtils.isRunningInTest()) {
+ Log.e(
+ TAG,
+ "Not disabling for test: " + this,
new IllegalStateException(DETAIL_MESSAGE));
} else {
mTestValue = false;
@@ -81,7 +77,7 @@ public class TestableFeature implements Feature {
@VisibleForTesting
public void resetForTests() {
- if (!TvCommonUtils.isRunningInTest()) {
+ if (!CommonUtils.isRunningInTest()) {
Log.e(TAG, "Not resetting feature: " + this, new IllegalStateException(DETAIL_MESSAGE));
} else {
mTestValue = null;
@@ -90,7 +86,7 @@ public class TestableFeature implements Feature {
@Override
public boolean isEnabled(Context context) {
- if (TvCommonUtils.isRunningInTest() && mTestValue != null) {
+ if (CommonUtils.isRunningInTest() && mTestValue != null) {
return mTestValue;
}
return mDelegate.isEnabled(context);
@@ -99,7 +95,7 @@ public class TestableFeature implements Feature {
@Override
public String toString() {
String msg = mDelegate.toString();
- if (TvCommonUtils.isRunningInTest()) {
+ if (CommonUtils.isRunningInTest()) {
if (mTestValue == null) {
msg = "Testable Feature is unchanged: " + msg;
} else {
diff --git a/common/src/com/android/tv/common/MemoryManageable.java b/common/src/com/android/tv/common/memory/MemoryManageable.java
index 0cb36103..3e81fb5e 100644
--- a/common/src/com/android/tv/common/MemoryManageable.java
+++ b/common/src/com/android/tv/common/memory/MemoryManageable.java
@@ -14,16 +14,13 @@
* limitations under the License
*/
-package com.android.tv.common;
+package com.android.tv.common.memory;
/**
- * Interface for the fine-grained memory management.
- * The class which wants to release memory based on the system constraints should inherit
- * this interface and implement {@link #performTrimMemory}.
+ * Interface for the fine-grained memory management. The class which wants to release memory based
+ * on the system constraints should inherit this interface and implement {@link #performTrimMemory}.
*/
public interface MemoryManageable {
- /**
- * For more information, see {@link android.content.ComponentCallbacks2#onTrimMemory}.
- */
+ /** For more information, see {@link android.content.ComponentCallbacks2#onTrimMemory}. */
void performTrimMemory(int level);
}
diff --git a/common/src/com/android/tv/common/recording/RecordingCapability.java b/common/src/com/android/tv/common/recording/RecordingCapability.java
index 266fd271..9fef527c 100644
--- a/common/src/com/android/tv/common/recording/RecordingCapability.java
+++ b/common/src/com/android/tv/common/recording/RecordingCapability.java
@@ -18,16 +18,11 @@ package com.android.tv.common.recording;
import android.os.Parcel;
import android.os.Parcelable;
-
import java.util.Objects;
-/**
- * Static representation of the recording capability of a TvInputService.
- */
-public final class RecordingCapability implements Parcelable{
- /**
- * The inputId this capability represents.
- */
+/** Static representation of the recording capability of a TvInputService. */
+public final class RecordingCapability implements Parcelable {
+ /** The inputId this capability represents. */
public final String inputId;
/**
@@ -40,8 +35,8 @@ public final class RecordingCapability implements Parcelable{
/**
* The max number concurrent session that play a stream.
*
- *<p>This is often limited by the number of decoders available.
- * The count includes both playing live TV and playing a recorded stream.
+ * <p>This is often limited by the number of decoders available. The count includes both playing
+ * live TV and playing a recorded stream.
*/
public final int maxConcurrentPlayingSessions;
@@ -52,13 +47,14 @@ public final class RecordingCapability implements Parcelable{
*/
public final int maxConcurrentSessionsOfAllTypes;
- /**
- * True if a tuned session can support recording and playback from the same resource.
- */
+ /** True if a tuned session can support recording and playback from the same resource. */
public final boolean playbackWhileRecording;
- private RecordingCapability(String inputId, int maxConcurrentTunedSessions,
- int maxConcurrentPlayingSessions, int maxConcurrentSessionsOfAllTypes,
+ private RecordingCapability(
+ String inputId,
+ int maxConcurrentTunedSessions,
+ int maxConcurrentPlayingSessions,
+ int maxConcurrentSessionsOfAllTypes,
boolean playbackWhileRecording) {
this.inputId = inputId;
this.maxConcurrentTunedSessions = maxConcurrentTunedSessions;
@@ -93,12 +89,12 @@ public final class RecordingCapability implements Parcelable{
return false;
}
RecordingCapability that = (RecordingCapability) o;
- return Objects.equals(maxConcurrentTunedSessions, that.maxConcurrentTunedSessions) &&
- Objects.equals(maxConcurrentPlayingSessions, that.maxConcurrentPlayingSessions) &&
- Objects.equals(maxConcurrentSessionsOfAllTypes,
- that.maxConcurrentSessionsOfAllTypes) &&
- Objects.equals(playbackWhileRecording, that.playbackWhileRecording) &&
- Objects.equals(inputId, that.inputId);
+ return Objects.equals(maxConcurrentTunedSessions, that.maxConcurrentTunedSessions)
+ && Objects.equals(maxConcurrentPlayingSessions, that.maxConcurrentPlayingSessions)
+ && Objects.equals(
+ maxConcurrentSessionsOfAllTypes, that.maxConcurrentSessionsOfAllTypes)
+ && Objects.equals(playbackWhileRecording, that.playbackWhileRecording)
+ && Objects.equals(inputId, that.inputId);
}
@Override
@@ -108,13 +104,19 @@ public final class RecordingCapability implements Parcelable{
@Override
public String toString() {
- return "RecordingCapability{" +
- "inputId='" + inputId + '\'' +
- ", maxConcurrentTunedSessions=" + maxConcurrentTunedSessions +
- ", maxConcurrentPlayingSessions=" + maxConcurrentPlayingSessions +
- ", maxConcurrentSessionsOfAllTypes=" + maxConcurrentSessionsOfAllTypes +
- ", playbackWhileRecording=" + playbackWhileRecording +
- '}';
+ return "RecordingCapability{"
+ + "inputId='"
+ + inputId
+ + '\''
+ + ", maxConcurrentTunedSessions="
+ + maxConcurrentTunedSessions
+ + ", maxConcurrentPlayingSessions="
+ + maxConcurrentPlayingSessions
+ + ", maxConcurrentSessionsOfAllTypes="
+ + maxConcurrentSessionsOfAllTypes
+ + ", playbackWhileRecording="
+ + playbackWhileRecording
+ + '}';
}
@Override
@@ -122,17 +124,18 @@ public final class RecordingCapability implements Parcelable{
return 0;
}
- public static final Creator<RecordingCapability> CREATOR = new Creator<RecordingCapability>() {
- @Override
- public RecordingCapability createFromParcel(Parcel in) {
- return new RecordingCapability(in);
- }
+ public static final Creator<RecordingCapability> CREATOR =
+ new Creator<RecordingCapability>() {
+ @Override
+ public RecordingCapability createFromParcel(Parcel in) {
+ return new RecordingCapability(in);
+ }
- @Override
- public RecordingCapability[] newArray(int size) {
- return new RecordingCapability[size];
- }
- };
+ @Override
+ public RecordingCapability[] newArray(int size) {
+ return new RecordingCapability[size];
+ }
+ };
public static Builder builder() {
return new Builder();
@@ -171,11 +174,12 @@ public final class RecordingCapability implements Parcelable{
}
public RecordingCapability build() {
- return new RecordingCapability(mInputId, mMaxConcurrentTunedSessions,
- mMaxConcurrentPlayingSessions, mMaxConcurrentSessionsOfAllTypes,
+ return new RecordingCapability(
+ mInputId,
+ mMaxConcurrentTunedSessions,
+ mMaxConcurrentPlayingSessions,
+ mMaxConcurrentSessionsOfAllTypes,
mPlaybackWhileRecording);
}
}
}
-
-
diff --git a/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java b/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java
new file mode 100644
index 00000000..8b45a730
--- /dev/null
+++ b/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.common.recording;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Environment;
+import android.os.Looper;
+import android.os.StatFs;
+import android.support.annotation.AnyThread;
+import android.support.annotation.IntDef;
+import android.support.annotation.WorkerThread;
+import android.util.Log;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.feature.CommonFeatures;
+import java.io.File;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/** Signals DVR storage status change such as plugging/unplugging. */
+public class RecordingStorageStatusManager {
+ private static final String TAG = "RecordingStorageStatusManager";
+ private static final boolean DEBUG = false;
+
+ /** Minimum storage size to support DVR */
+ public static final long MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES = 50 * 1024 * 1024 * 1024L; // 50GB
+
+ private static final long MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES =
+ 10 * 1024 * 1024 * 1024L; // 10GB
+ private static final String RECORDING_DATA_SUB_PATH = "/recording";
+
+ /** Storage status constants. */
+ @IntDef({
+ STORAGE_STATUS_OK,
+ STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL,
+ STORAGE_STATUS_FREE_SPACE_INSUFFICIENT,
+ STORAGE_STATUS_MISSING
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StorageStatus {}
+
+ /** Current storage is OK to record a program. */
+ public static final int STORAGE_STATUS_OK = 0;
+
+ /** Current storage's total capacity is smaller than DVR requirement. */
+ public static final int STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL = 1;
+
+ /** Current storage's free space is insufficient to record programs. */
+ public static final int STORAGE_STATUS_FREE_SPACE_INSUFFICIENT = 2;
+
+ /** Current storage is missing. */
+ public static final int STORAGE_STATUS_MISSING = 3;
+
+ private final Context mContext;
+ private final Set<OnStorageMountChangedListener> mOnStorageMountChangedListeners =
+ new CopyOnWriteArraySet<>();
+ private MountedStorageStatus mMountedStorageStatus;
+ private boolean mStorageValid;
+
+ private class MountedStorageStatus {
+ private final boolean mStorageMounted;
+ private final File mStorageMountedDir;
+ private final long mStorageMountedCapacity;
+
+ private MountedStorageStatus(boolean mounted, File mountedDir, long capacity) {
+ mStorageMounted = mounted;
+ mStorageMountedDir = mountedDir;
+ mStorageMountedCapacity = capacity;
+ }
+
+ private boolean isValidForDvr() {
+ return mStorageMounted && mStorageMountedCapacity >= MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof MountedStorageStatus)) {
+ return false;
+ }
+ MountedStorageStatus status = (MountedStorageStatus) other;
+ return mStorageMounted == status.mStorageMounted
+ && Objects.equals(mStorageMountedDir, status.mStorageMountedDir)
+ && mStorageMountedCapacity == status.mStorageMountedCapacity;
+ }
+ }
+
+ public interface OnStorageMountChangedListener {
+
+ /**
+ * Listener for DVR storage status change.
+ *
+ * @param storageMounted {@code true} when DVR possible storage is mounted, {@code false}
+ * otherwise.
+ */
+ void onStorageMountChanged(boolean storageMounted);
+ }
+
+ private final class StorageStatusBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ MountedStorageStatus result = getStorageStatusInternal();
+ if (mMountedStorageStatus.equals(result)) {
+ return;
+ }
+ mMountedStorageStatus = result;
+ if (result.mStorageMounted) {
+ cleanUpDbIfNeeded();
+ }
+ boolean valid = result.isValidForDvr();
+ if (valid == mStorageValid) {
+ return;
+ }
+ mStorageValid = valid;
+ for (OnStorageMountChangedListener l : mOnStorageMountChangedListeners) {
+ l.onStorageMountChanged(valid);
+ }
+ }
+ }
+
+ /**
+ * Creates RecordingStorageStatusManager.
+ *
+ * @param context {@link Context}
+ */
+ public RecordingStorageStatusManager(final Context context) {
+ mContext = context;
+ mMountedStorageStatus = getStorageStatusInternal();
+ mStorageValid = mMountedStorageStatus.isValidForDvr();
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_MEDIA_MOUNTED);
+ filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
+ filter.addAction(Intent.ACTION_MEDIA_EJECT);
+ filter.addAction(Intent.ACTION_MEDIA_REMOVED);
+ filter.addAction(Intent.ACTION_MEDIA_BAD_REMOVAL);
+ filter.addDataScheme(ContentResolver.SCHEME_FILE);
+ mContext.registerReceiver(new StorageStatusBroadcastReceiver(), filter);
+ }
+
+ /**
+ * Adds the listener for receiving storage status change.
+ *
+ * @param listener
+ */
+ public void addListener(OnStorageMountChangedListener listener) {
+ mOnStorageMountChangedListeners.add(listener);
+ }
+
+ /** Removes the current listener. */
+ public void removeListener(OnStorageMountChangedListener listener) {
+ mOnStorageMountChangedListeners.remove(listener);
+ }
+
+ /** Returns true if a storage is mounted. */
+ public boolean isStorageMounted() {
+ return mMountedStorageStatus.mStorageMounted;
+ }
+
+ /** Returns the path to DVR recording data directory. This can take for a while sometimes. */
+ @WorkerThread
+ public File getRecordingRootDataDirectory() {
+ SoftPreconditions.checkState(Looper.myLooper() != Looper.getMainLooper());
+ if (mMountedStorageStatus.mStorageMountedDir == null) {
+ return null;
+ }
+ File root = mContext.getExternalFilesDir(null);
+ String rootPath;
+ try {
+ rootPath = root != null ? root.getCanonicalPath() : null;
+ } catch (IOException | SecurityException e) {
+ return null;
+ }
+ return rootPath == null ? null : new File(rootPath + RECORDING_DATA_SUB_PATH);
+ }
+
+ /**
+ * Returns the current storage status for DVR recordings.
+ *
+ * @return {@link StorageStatus}
+ */
+ @AnyThread
+ public @StorageStatus int getDvrStorageStatus() {
+ MountedStorageStatus status = mMountedStorageStatus;
+ if (status.mStorageMountedDir == null) {
+ return STORAGE_STATUS_MISSING;
+ }
+ if (CommonFeatures.FORCE_RECORDING_UNTIL_NO_SPACE.isEnabled(mContext)) {
+ return STORAGE_STATUS_OK;
+ }
+ if (status.mStorageMountedCapacity < MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES) {
+ return STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL;
+ }
+ try {
+ StatFs statFs = new StatFs(status.mStorageMountedDir.toString());
+ if (statFs.getAvailableBytes() < MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES) {
+ return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT;
+ }
+ } catch (IllegalArgumentException e) {
+ // In rare cases, storage status change was not notified yet.
+ SoftPreconditions.checkState(false);
+ return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT;
+ }
+ return STORAGE_STATUS_OK;
+ }
+
+ /**
+ * Returns whether the storage has sufficient storage.
+ *
+ * @return {@code true} when there is sufficient storage, {@code false} otherwise
+ */
+ public boolean isStorageSufficient() {
+ return getDvrStorageStatus() == STORAGE_STATUS_OK;
+ }
+
+ /** APPs that want to clean up DB for recordings should override this method to do the job. */
+ protected void cleanUpDbIfNeeded() {}
+
+ private MountedStorageStatus getStorageStatusInternal() {
+ boolean storageMounted =
+ Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
+ File storageMountedDir = storageMounted ? Environment.getExternalStorageDirectory() : null;
+ storageMounted = storageMounted && storageMountedDir != null;
+ long storageMountedCapacity = 0L;
+ if (storageMounted) {
+ try {
+ StatFs statFs = new StatFs(storageMountedDir.toString());
+ storageMountedCapacity = statFs.getTotalBytes();
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Storage mount status was changed.");
+ storageMounted = false;
+ storageMountedDir = null;
+ }
+ }
+ return new MountedStorageStatus(storageMounted, storageMountedDir, storageMountedCapacity);
+ }
+}
diff --git a/common/src/com/android/tv/common/ui/setup/OnActionClickListener.java b/common/src/com/android/tv/common/ui/setup/OnActionClickListener.java
index 392d489f..6f088c0b 100644
--- a/common/src/com/android/tv/common/ui/setup/OnActionClickListener.java
+++ b/common/src/com/android/tv/common/ui/setup/OnActionClickListener.java
@@ -18,14 +18,12 @@ package com.android.tv.common.ui.setup;
import android.os.Bundle;
-/**
- * A listener for the action click.
- */
+/** A listener for the action click. */
public interface OnActionClickListener {
/**
* Called when the action is clicked.
- * <p>
- * The method should return {@code true} if the action is handled, otherwise {@code false}.
+ *
+ * <p>The method should return {@code true} if the action is handled, otherwise {@code false}.
*
* @param category The action category.
* @param id The action id.
diff --git a/common/src/com/android/tv/common/ui/setup/SetupActionHelper.java b/common/src/com/android/tv/common/ui/setup/SetupActionHelper.java
index 7ee06faf..8a7dbd70 100644
--- a/common/src/com/android/tv/common/ui/setup/SetupActionHelper.java
+++ b/common/src/com/android/tv/common/ui/setup/SetupActionHelper.java
@@ -22,46 +22,45 @@ import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
-/**
- * Helper class for the execution in the fragment.
- */
+/** Helper class for the execution in the fragment. */
public class SetupActionHelper {
private static final String TAG = "SetupActionHelper";
- /**
- * Executes the action.
- */
+ /** Executes the action. */
public static boolean onActionClick(Fragment fragment, String category, int actionId) {
return onActionClick(fragment, category, actionId, null);
}
- /**
- * Executes the action.
- */
- public static boolean onActionClick(Fragment fragment, String category, int actionId,
- Bundle params) {
+ /** Executes the action. */
+ public static boolean onActionClick(
+ Fragment fragment, String category, int actionId, Bundle params) {
if (fragment.getActivity() instanceof OnActionClickListener) {
- return ((OnActionClickListener) fragment.getActivity()).onActionClick(category,
- actionId, params);
+ return ((OnActionClickListener) fragment.getActivity())
+ .onActionClick(category, actionId, params);
}
- Log.e(TAG, "Activity can't handle the action: {category=" + category + ", actionId="
- + actionId + ", params=" + params + "}");
+ Log.e(
+ TAG,
+ "Activity can't handle the action: {category="
+ + category
+ + ", actionId="
+ + actionId
+ + ", params="
+ + params
+ + "}");
return false;
}
- /**
- * Creates an {@link OnClickListener} to handle the action.
- */
- public static OnClickListener createOnClickListenerForAction(Fragment fragment, String category,
- int actionId, Bundle params) {
+ /** Creates an {@link OnClickListener} to handle the action. */
+ public static OnClickListener createOnClickListenerForAction(
+ Fragment fragment, String category, int actionId, Bundle params) {
return new OnActionClickListenerForAction(fragment, category, actionId, params);
}
/**
* The {@link OnClickListener} for the view.
- * <p>
- * Note that this class should be used only for the views in the {@code mFragment} to avoid the
- * leak of mFragment.
+ *
+ * <p>Note that this class should be used only for the views in the {@code mFragment} to avoid
+ * the leak of mFragment.
*/
private static class OnActionClickListenerForAction implements OnClickListener {
private final Fragment mFragment;
@@ -69,8 +68,8 @@ public class SetupActionHelper {
private final int mActionId;
private final Bundle mParams;
- OnActionClickListenerForAction(Fragment fragment, String category, int actionId,
- Bundle params) {
+ OnActionClickListenerForAction(
+ Fragment fragment, String category, int actionId, Bundle params) {
mFragment = fragment;
mCategory = category;
mActionId = actionId;
@@ -83,5 +82,5 @@ public class SetupActionHelper {
}
}
- private SetupActionHelper() { }
+ private SetupActionHelper() {}
}
diff --git a/common/src/com/android/tv/common/ui/setup/SetupActivity.java b/common/src/com/android/tv/common/ui/setup/SetupActivity.java
index 2b381a6e..67418ce0 100644
--- a/common/src/com/android/tv/common/ui/setup/SetupActivity.java
+++ b/common/src/com/android/tv/common/ui/setup/SetupActivity.java
@@ -28,7 +28,6 @@ import android.transition.Transition;
import android.transition.TransitionInflater;
import android.view.View;
import android.view.ViewTreeObserver.OnPreDrawListener;
-
import com.android.tv.common.R;
import com.android.tv.common.WeakHandler;
import com.android.tv.common.ui.setup.animation.SetupAnimationHelper;
@@ -51,23 +50,28 @@ public abstract class SetupActivity extends Activity implements OnActionClickLis
super.onCreate(savedInstanceState);
SetupAnimationHelper.initialize(this);
setContentView(R.layout.activity_setup);
- mFragmentTransitionDuration = getResources().getInteger(
- R.integer.setup_fragment_transition_duration);
+ mFragmentTransitionDuration =
+ getResources().getInteger(R.integer.setup_fragment_transition_duration);
// Show initial fragment only when the saved state is not restored, because the last
// fragment is restored if savesInstanceState is not null.
if (savedInstanceState == null) {
// This is the workaround to show the first fragment with delay to show the fragment
// enter transition. See http://b/26255145
- getWindow().getDecorView().getViewTreeObserver().addOnPreDrawListener(
- new OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- getWindow().getDecorView().getViewTreeObserver()
- .removeOnPreDrawListener(this);
- showInitialFragment();
- return true;
- }
- });
+ getWindow()
+ .getDecorView()
+ .getViewTreeObserver()
+ .addOnPreDrawListener(
+ new OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ getWindow()
+ .getDecorView()
+ .getViewTreeObserver()
+ .removeOnPreDrawListener(this);
+ showInitialFragment();
+ return true;
+ }
+ });
} else {
mShowInitialFragment = false;
}
@@ -76,8 +80,8 @@ public abstract class SetupActivity extends Activity implements OnActionClickLis
/**
* The inherited class should provide the initial fragment to show.
*
- * <p>If this method returns {@code null} during {@link #onCreate}, then call
- * {@link #showInitialFragment} explicitly later with non null initial fragment.
+ * <p>If this method returns {@code null} during {@link #onCreate}, then call {@link
+ * #showInitialFragment} explicitly later with non null initial fragment.
*/
protected abstract Fragment onCreateInitialFragment();
@@ -98,16 +102,15 @@ public abstract class SetupActivity extends Activity implements OnActionClickLis
}
}
- /**
- * Shows the given fragment.
- */
+ /** Shows the given fragment. */
protected FragmentTransaction showFragment(Fragment fragment, boolean addToBackStack) {
FragmentTransaction ft = getFragmentManager().beginTransaction();
if (fragment instanceof SetupFragment) {
int[] sharedElements = ((SetupFragment) fragment).getSharedElementIds();
if (sharedElements != null && sharedElements.length > 0) {
- Transition sharedTransition = TransitionInflater.from(this)
- .inflateTransition(R.transition.transition_action_background);
+ Transition sharedTransition =
+ TransitionInflater.from(this)
+ .inflateTransition(R.transition.transition_action_background);
sharedTransition.setDuration(getSharedElementTransitionDuration());
SetupAnimationHelper.applyAnimationTimeScale(sharedTransition);
fragment.setSharedElementEnterTransition(sharedTransition);
@@ -143,9 +146,9 @@ public abstract class SetupActivity extends Activity implements OnActionClickLis
/**
* Override this method if the inherited class wants to handle the action.
- * <p>
- * The override method should return {@code true} if the action is handled, otherwise
- * {@code false}.
+ *
+ * <p>The override method should return {@code true} if the action is handled, otherwise {@code
+ * false}.
*/
protected boolean executeAction(String category, int actionId, Bundle params) {
return false;
diff --git a/common/src/com/android/tv/common/ui/setup/SetupFragment.java b/common/src/com/android/tv/common/ui/setup/SetupFragment.java
index d2b9d7c8..7d47548d 100644
--- a/common/src/com/android/tv/common/ui/setup/SetupFragment.java
+++ b/common/src/com/android/tv/common/ui/setup/SetupFragment.java
@@ -26,22 +26,25 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
-
import com.android.tv.common.ui.setup.animation.FadeAndShortSlide;
import com.android.tv.common.ui.setup.animation.SetupAnimationHelper;
-
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-/**
- * A fragment which slides when it is entering/exiting.
- */
+/** A fragment which slides when it is entering/exiting. */
public abstract class SetupFragment extends Fragment {
@Retention(RetentionPolicy.SOURCE)
- @IntDef(flag = true,
- value = {FRAGMENT_ENTER_TRANSITION, FRAGMENT_EXIT_TRANSITION,
- FRAGMENT_REENTER_TRANSITION, FRAGMENT_RETURN_TRANSITION})
+ @IntDef(
+ flag = true,
+ value = {
+ FRAGMENT_ENTER_TRANSITION,
+ FRAGMENT_EXIT_TRANSITION,
+ FRAGMENT_REENTER_TRANSITION,
+ FRAGMENT_RETURN_TRANSITION
+ }
+ )
public @interface FragmentTransitionType {}
+
public static final int FRAGMENT_ENTER_TRANSITION = 0x01;
public static final int FRAGMENT_EXIT_TRANSITION = FRAGMENT_ENTER_TRANSITION << 1;
public static final int FRAGMENT_REENTER_TRANSITION = FRAGMENT_ENTER_TRANSITION << 2;
@@ -49,50 +52,50 @@ public abstract class SetupFragment extends Fragment {
private boolean mEnterTransitionRunning;
- private final TransitionListener mTransitionListener = new TransitionListener() {
- @Override
- public void onTransitionStart(Transition transition) {
- mEnterTransitionRunning = true;
- }
+ private final TransitionListener mTransitionListener =
+ new TransitionListener() {
+ @Override
+ public void onTransitionStart(Transition transition) {
+ mEnterTransitionRunning = true;
+ }
- @Override
- public void onTransitionEnd(Transition transition) {
- mEnterTransitionRunning = false;
- onEnterTransitionEnd();
- }
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ mEnterTransitionRunning = false;
+ onEnterTransitionEnd();
+ }
- @Override
- public void onTransitionCancel(Transition transition) { }
+ @Override
+ public void onTransitionCancel(Transition transition) {}
- @Override
- public void onTransitionPause(Transition transition) { }
+ @Override
+ public void onTransitionPause(Transition transition) {}
- @Override
- public void onTransitionResume(Transition transition) { }
- };
+ @Override
+ public void onTransitionResume(Transition transition) {}
+ };
- /**
- * Returns {@code true} if the enter/reenter transition is running.
- */
+ /** Returns {@code true} if the enter/reenter transition is running. */
protected boolean isEnterTransitionRunning() {
return mEnterTransitionRunning;
}
- /**
- * Called when the enter/reenter transition ends.
- */
- protected void onEnterTransitionEnd() { }
+ /** Called when the enter/reenter transition ends. */
+ protected void onEnterTransitionEnd() {}
public SetupFragment() {
setAllowEnterTransitionOverlap(false);
setAllowReturnTransitionOverlap(false);
- enableFragmentTransition(FRAGMENT_ENTER_TRANSITION | FRAGMENT_EXIT_TRANSITION
- | FRAGMENT_REENTER_TRANSITION | FRAGMENT_RETURN_TRANSITION);
+ enableFragmentTransition(
+ FRAGMENT_ENTER_TRANSITION
+ | FRAGMENT_EXIT_TRANSITION
+ | FRAGMENT_REENTER_TRANSITION
+ | FRAGMENT_RETURN_TRANSITION);
}
@Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(getLayoutResourceId(), container, false);
// After the transition animation, we need to request the focus. If not, this fragment
// doesn't have the focus.
@@ -100,18 +103,17 @@ public abstract class SetupFragment extends Fragment {
return view;
}
- /**
- * Returns the layout resource ID for this fragment.
- */
- abstract protected int getLayoutResourceId();
+ /** Returns the layout resource ID for this fragment. */
+ protected abstract int getLayoutResourceId();
protected void setOnClickAction(View view, final String category, final int actionId) {
- view.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View view) {
- onActionClick(category, actionId);
- }
- });
+ view.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ onActionClick(category, actionId);
+ }
+ });
}
protected boolean onActionClick(String category, int actionId) {
@@ -141,24 +143,22 @@ public abstract class SetupFragment extends Fragment {
/**
* Enables fragment transition according to the given {@code mask}.
*
- * @param mask This value is the combination of {@link #FRAGMENT_ENTER_TRANSITION},
- * {@link #FRAGMENT_EXIT_TRANSITION}, {@link #FRAGMENT_REENTER_TRANSITION}, and
- * {@link #FRAGMENT_RETURN_TRANSITION}.
+ * @param mask This value is the combination of {@link #FRAGMENT_ENTER_TRANSITION}, {@link
+ * #FRAGMENT_EXIT_TRANSITION}, {@link #FRAGMENT_REENTER_TRANSITION}, and {@link
+ * #FRAGMENT_RETURN_TRANSITION}.
*/
public void enableFragmentTransition(@FragmentTransitionType int mask) {
- setEnterTransition((mask & FRAGMENT_ENTER_TRANSITION) == 0 ? null
- : createTransition(Gravity.END));
- setExitTransition((mask & FRAGMENT_EXIT_TRANSITION) == 0 ? null
- : createTransition(Gravity.START));
- setReenterTransition((mask & FRAGMENT_REENTER_TRANSITION) == 0 ? null
- : createTransition(Gravity.START));
- setReturnTransition((mask & FRAGMENT_RETURN_TRANSITION) == 0 ? null
- : createTransition(Gravity.END));
+ setEnterTransition(
+ (mask & FRAGMENT_ENTER_TRANSITION) == 0 ? null : createTransition(Gravity.END));
+ setExitTransition(
+ (mask & FRAGMENT_EXIT_TRANSITION) == 0 ? null : createTransition(Gravity.START));
+ setReenterTransition(
+ (mask & FRAGMENT_REENTER_TRANSITION) == 0 ? null : createTransition(Gravity.START));
+ setReturnTransition(
+ (mask & FRAGMENT_RETURN_TRANSITION) == 0 ? null : createTransition(Gravity.END));
}
- /**
- * Sets the transition with the given {@code slidEdge}.
- */
+ /** Sets the transition with the given {@code slidEdge}. */
public void setFragmentTransition(@FragmentTransitionType int transitionType, int slideEdge) {
switch (transitionType) {
case FRAGMENT_ENTER_TRANSITION:
@@ -184,9 +184,7 @@ public abstract class SetupFragment extends Fragment {
.build();
}
- /**
- * Changes the move distance of the transitions to short distance.
- */
+ /** Changes the move distance of the transitions to short distance. */
public void setShortDistance(@FragmentTransitionType int mask) {
if ((mask & FRAGMENT_ENTER_TRANSITION) != 0) {
Transition transition = getEnterTransition();
@@ -218,7 +216,7 @@ public abstract class SetupFragment extends Fragment {
* Returns the ID's of the view's whose descendants will perform delayed move.
*
* @see com.android.tv.common.ui.setup.animation.SetupAnimationHelper.TransitionBuilder
- * #setParentIdsForDelay
+ * #setParentIdsForDelay
*/
protected int[] getParentIdsForDelay() {
return null;
@@ -228,7 +226,7 @@ public abstract class SetupFragment extends Fragment {
* Sets the ID's of the views which will not be included in the transition.
*
* @see com.android.tv.common.ui.setup.animation.SetupAnimationHelper.TransitionBuilder
- * #setExcludeIds
+ * #setExcludeIds
*/
protected int[] getExcludedTargetIds() {
return null;
diff --git a/common/src/com/android/tv/common/ui/setup/SetupGuidedStepFragment.java b/common/src/com/android/tv/common/ui/setup/SetupGuidedStepFragment.java
index 88159da9..3c76c269 100644
--- a/common/src/com/android/tv/common/ui/setup/SetupGuidedStepFragment.java
+++ b/common/src/com/android/tv/common/ui/setup/SetupGuidedStepFragment.java
@@ -16,22 +16,26 @@
package com.android.tv.common.ui.setup;
+import static android.content.Context.ACCESSIBILITY_SERVICE;
+
import android.os.Bundle;
import android.support.v17.leanback.app.GuidedStepFragment;
import android.support.v17.leanback.widget.GuidanceStylist;
import android.support.v17.leanback.widget.GuidedAction;
+import android.support.v17.leanback.widget.GuidedActionsStylist;
import android.support.v17.leanback.widget.VerticalGridView;
import android.view.LayoutInflater;
import android.view.View;
+import android.view.View.AccessibilityDelegate;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.LinearLayout;
-
import com.android.tv.common.R;
-/**
- * A fragment for channel source info/setup.
- */
+/** A fragment for channel source info/setup. */
public abstract class SetupGuidedStepFragment extends GuidedStepFragment {
/**
* Key of the argument which indicate whether the parent of this fragment has three panes.
@@ -40,23 +44,32 @@ public abstract class SetupGuidedStepFragment extends GuidedStepFragment {
*/
public static final String KEY_THREE_PANE = "key_three_pane";
+ private View mContentFragment;
+ private boolean mFromContentFragment;
+ private boolean mAccessibilityMode;
+
@Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
Bundle arguments = getArguments();
- view.findViewById(R.id.action_fragment_root).setPadding(0, 0, 0, 0);
- LinearLayout.LayoutParams guidanceLayoutParams = (LinearLayout.LayoutParams)
- view.findViewById(R.id.content_fragment).getLayoutParams();
+ view.findViewById(android.support.v17.leanback.R.id.action_fragment_root)
+ .setPadding(0, 0, 0, 0);
+ mContentFragment = view.findViewById(android.support.v17.leanback.R.id.content_fragment);
+ LinearLayout.LayoutParams guidanceLayoutParams =
+ (LinearLayout.LayoutParams) mContentFragment.getLayoutParams();
guidanceLayoutParams.weight = 0;
if (arguments != null && arguments.getBoolean(KEY_THREE_PANE, false)) {
// Content fragment.
- guidanceLayoutParams.width = getResources().getDimensionPixelOffset(
- R.dimen.setup_guidedstep_guidance_section_width_3pane);
- int doneButtonWidth = getResources().getDimensionPixelOffset(
- R.dimen.setup_done_button_container_width);
+ guidanceLayoutParams.width =
+ getResources()
+ .getDimensionPixelOffset(
+ R.dimen.setup_guidedstep_guidance_section_width_3pane);
+ int doneButtonWidth =
+ getResources()
+ .getDimensionPixelOffset(R.dimen.setup_done_button_container_width);
// Guided actions list
- View list = view.findViewById(R.id.guidedactions_list);
+ View list = view.findViewById(android.support.v17.leanback.R.id.guidedactions_list);
MarginLayoutParams marginLayoutParams = (MarginLayoutParams) list.getLayoutParams();
// Use content view to check layout direction while view is being created.
if (getResources().getConfiguration().getLayoutDirection()
@@ -67,31 +80,68 @@ public abstract class SetupGuidedStepFragment extends GuidedStepFragment {
}
} else {
// Content fragment.
- guidanceLayoutParams.width = getResources().getDimensionPixelOffset(
- R.dimen.setup_guidedstep_guidance_section_width_2pane);
+ guidanceLayoutParams.width =
+ getResources()
+ .getDimensionPixelOffset(
+ R.dimen.setup_guidedstep_guidance_section_width_2pane);
}
// gridView Alignment
VerticalGridView gridView = getGuidedActionsStylist().getActionsGridView();
- int offset = getResources().getDimensionPixelOffset(
- R.dimen.setup_guidedactions_selector_margin_top);
+ int offset =
+ getResources()
+ .getDimensionPixelOffset(R.dimen.setup_guidedactions_selector_margin_top);
gridView.setWindowAlignmentOffset(offset);
gridView.setWindowAlignmentOffsetPercent(0);
gridView.setItemAlignmentOffsetPercent(0);
- ((ViewGroup) view.findViewById(R.id.guidedactions_list)).setTransitionGroup(false);
+ ((ViewGroup) view.findViewById(android.support.v17.leanback.R.id.guidedactions_list))
+ .setTransitionGroup(false);
// Needed for the shared element transition.
// content_frame is defined in leanback.
- ViewGroup group = (ViewGroup) view.findViewById(R.id.content_frame);
+ ViewGroup group =
+ (ViewGroup) view.findViewById(android.support.v17.leanback.R.id.content_frame);
group.setClipChildren(false);
group.setClipToPadding(false);
return view;
}
@Override
+ public GuidedActionsStylist onCreateActionsStylist() {
+ return new SetupGuidedStepFragmentGuidedActionsStylist();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ AccessibilityManager am =
+ (AccessibilityManager) getActivity().getSystemService(ACCESSIBILITY_SERVICE);
+ mAccessibilityMode = am != null && am.isEnabled() && am.isTouchExplorationEnabled();
+ mContentFragment.setFocusable(mAccessibilityMode);
+ if (mAccessibilityMode) {
+ mContentFragment.setAccessibilityDelegate(
+ new AccessibilityDelegate() {
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS
+ && !getActions().isEmpty()) {
+ // scroll to the top. This makes the first action view on the screen.
+ // Otherwise, the view can be recycled, so accessibility events cannot
+ // be sent later.
+ getGuidedActionsStylist().getActionsGridView().scrollToPosition(0);
+ mFromContentFragment = true;
+ }
+ return super.performAccessibilityAction(host, action, args);
+ }
+ });
+ mContentFragment.requestFocus();
+ }
+ }
+
+ @Override
public GuidanceStylist onCreateGuidanceStylist() {
return new GuidanceStylist() {
@Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Guidance guidance) {
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Guidance guidance) {
View view = super.onCreateView(inflater, container, guidance);
if (guidance.getIconDrawable() == null) {
// Icon view should not take up space when we don't use image.
@@ -102,10 +152,19 @@ public abstract class SetupGuidedStepFragment extends GuidedStepFragment {
};
}
- abstract protected String getActionCategory();
+ protected abstract String getActionCategory();
+
+ protected View getDoneButton() {
+ return getActivity().findViewById(R.id.button_done);
+ }
@Override
public void onGuidedActionClicked(GuidedAction action) {
+ if (!action.isFocusable()) {
+ // an unfocusable action may be clicked in accessibility mode when it's accessibility
+ // focused
+ return;
+ }
SetupActionHelper.onActionClick(this, getActionCategory(), (int) action.getId());
}
@@ -118,4 +177,38 @@ public abstract class SetupGuidedStepFragment extends GuidedStepFragment {
public boolean isFocusOutEndAllowed() {
return true;
}
+
+ protected void setAccessibilityDelegate(GuidedActionsStylist.ViewHolder vh,
+ GuidedAction action) {
+ if (!mAccessibilityMode || findActionPositionById(action.getId()) == 0) {
+ return;
+ }
+ vh.itemView.setAccessibilityDelegate(
+ new AccessibilityDelegate() {
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ if ((action == AccessibilityNodeInfo.ACTION_FOCUS
+ || action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS)
+ && mFromContentFragment) {
+ // block the action and make the first action view accessibility focused
+ View view = getActionItemView(0);
+ if (view != null) {
+ view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+ mFromContentFragment = false;
+ return true;
+ }
+ }
+ return super.performAccessibilityAction(host, action, args);
+ }
+ });
+ }
+
+ private class SetupGuidedStepFragmentGuidedActionsStylist extends GuidedActionsStylist {
+
+ @Override
+ public void onBindViewHolder(GuidedActionsStylist.ViewHolder vh, GuidedAction action) {
+ super.onBindViewHolder(vh, action);
+ setAccessibilityDelegate(vh, action);
+ }
+ }
}
diff --git a/common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java b/common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java
index b9ad4657..c02d3f56 100644
--- a/common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java
+++ b/common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java
@@ -23,34 +23,43 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
-
import com.android.tv.common.R;
-/**
- * A fragment for channel source info/setup.
- */
+/** A fragment for channel source info/setup. */
public abstract class SetupMultiPaneFragment extends SetupFragment {
private static final String TAG = "SetupMultiPaneFragment";
private static final boolean DEBUG = false;
public static final int ACTION_DONE = Integer.MAX_VALUE;
public static final int ACTION_SKIP = ACTION_DONE - 1;
+ public static final int MAX_SUBCLASSES_ID = ACTION_SKIP - 1;
- private static final String CONTENT_FRAGMENT_TAG = "content_fragment";
+ public static final String CONTENT_FRAGMENT_TAG = "content_fragment";
@Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (DEBUG) {
- Log.d(TAG, "onCreateView(" + inflater + ", " + container + ", " + savedInstanceState
- + ")");
+ Log.d(
+ TAG,
+ "onCreateView("
+ + inflater
+ + ", "
+ + container
+ + ", "
+ + savedInstanceState
+ + ")");
}
View view = super.onCreateView(inflater, container, savedInstanceState);
if (savedInstanceState == null) {
SetupGuidedStepFragment contentFragment = onCreateContentFragment();
- getChildFragmentManager().beginTransaction()
- .replace(R.id.guided_step_fragment_container, contentFragment,
- CONTENT_FRAGMENT_TAG).commit();
+ getChildFragmentManager()
+ .beginTransaction()
+ .replace(
+ R.id.guided_step_fragment_container,
+ contentFragment,
+ CONTENT_FRAGMENT_TAG)
+ .commit();
}
if (needsDoneButton()) {
setOnClickAction(view.findViewById(R.id.button_done), getActionCategory(), ACTION_DONE);
@@ -65,12 +74,12 @@ public abstract class SetupMultiPaneFragment extends SetupFragment {
if (getResources().getConfiguration().getLayoutDirection()
== View.LAYOUT_DIRECTION_LTR) {
((MarginLayoutParams) doneButtonContainer.getLayoutParams()).rightMargin =
- -getResources().getDimensionPixelOffset(
- R.dimen.setup_done_button_container_width);
+ -getResources()
+ .getDimensionPixelOffset(R.dimen.setup_done_button_container_width);
} else {
((MarginLayoutParams) doneButtonContainer.getLayoutParams()).leftMargin =
- -getResources().getDimensionPixelOffset(
- R.dimen.setup_done_button_container_width);
+ -getResources()
+ .getDimensionPixelOffset(R.dimen.setup_done_button_container_width);
}
view.findViewById(R.id.button_done).setFocusable(false);
}
@@ -82,15 +91,15 @@ public abstract class SetupMultiPaneFragment extends SetupFragment {
return R.layout.fragment_setup_multi_pane;
}
- abstract protected SetupGuidedStepFragment onCreateContentFragment();
+ protected abstract SetupGuidedStepFragment onCreateContentFragment();
@Nullable
protected SetupGuidedStepFragment getContentFragment() {
- return (SetupGuidedStepFragment) getChildFragmentManager()
- .findFragmentByTag(CONTENT_FRAGMENT_TAG);
+ return (SetupGuidedStepFragment)
+ getChildFragmentManager().findFragmentByTag(CONTENT_FRAGMENT_TAG);
}
- abstract protected String getActionCategory();
+ protected abstract String getActionCategory();
protected boolean needsDoneButton() {
return true;
@@ -102,11 +111,16 @@ public abstract class SetupMultiPaneFragment extends SetupFragment {
@Override
protected int[] getParentIdsForDelay() {
- return new int[] {R.id.content_fragment, R.id.guidedactions_list};
+ return new int[] {
+ android.support.v17.leanback.R.id.content_fragment,
+ android.support.v17.leanback.R.id.guidedactions_list
+ };
}
@Override
public int[] getSharedElementIds() {
- return new int[] {R.id.action_fragment_background, R.id.done_button_container};
+ return new int[] {
+ android.support.v17.leanback.R.id.action_fragment_background, R.id.done_button_container
+ };
}
-} \ No newline at end of file
+}
diff --git a/common/src/com/android/tv/common/ui/setup/animation/FadeAndShortSlide.java b/common/src/com/android/tv/common/ui/setup/animation/FadeAndShortSlide.java
index e1a8e60c..60ffb70f 100644
--- a/common/src/com/android/tv/common/ui/setup/animation/FadeAndShortSlide.java
+++ b/common/src/com/android/tv/common/ui/setup/animation/FadeAndShortSlide.java
@@ -29,15 +29,12 @@ import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
-
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
-/**
- * Execute horizontal slide of 1/4 width and fade (to workaround bug 23718734)
- */
+/** Execute horizontal slide of 1/4 width and fade (to workaround bug 23718734) */
public class FadeAndShortSlide extends Visibility {
private static final TimeInterpolator APPEAR_INTERPOLATOR = new DecelerateInterpolator();
private static final TimeInterpolator DISAPPEAR_INTERPOLATOR = new AccelerateInterpolator();
@@ -48,39 +45,45 @@ public class FadeAndShortSlide extends Visibility {
private static final int DEFAULT_DISTANCE = 200;
- private static abstract class CalculateSlide {
+ private abstract static class CalculateSlide {
/** Returns the translation value for view when it goes out of the scene */
- public abstract float getGoneX(ViewGroup sceneRoot, View view, int[] position,
- int distance);
+ public abstract float getGoneX(
+ ViewGroup sceneRoot, View view, int[] position, int distance);
}
- private static final CalculateSlide sCalculateStart = new CalculateSlide() {
- @Override
- public float getGoneX(ViewGroup sceneRoot, View view, int[] position, int distance) {
- final boolean isRtl = sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
- final float x;
- if (isRtl) {
- x = view.getTranslationX() + distance;
- } else {
- x = view.getTranslationX() - distance;
- }
- return x;
- }
- };
-
- private static final CalculateSlide sCalculateEnd = new CalculateSlide() {
- @Override
- public float getGoneX(ViewGroup sceneRoot, View view, int[] position, int distance) {
- final boolean isRtl = sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
- final float x;
- if (isRtl) {
- x = view.getTranslationX() - distance;
- } else {
- x = view.getTranslationX() + distance;
- }
- return x;
- }
- };
+ private static final CalculateSlide sCalculateStart =
+ new CalculateSlide() {
+ @Override
+ public float getGoneX(
+ ViewGroup sceneRoot, View view, int[] position, int distance) {
+ final boolean isRtl =
+ sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+ final float x;
+ if (isRtl) {
+ x = view.getTranslationX() + distance;
+ } else {
+ x = view.getTranslationX() - distance;
+ }
+ return x;
+ }
+ };
+
+ private static final CalculateSlide sCalculateEnd =
+ new CalculateSlide() {
+ @Override
+ public float getGoneX(
+ ViewGroup sceneRoot, View view, int[] position, int distance) {
+ final boolean isRtl =
+ sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+ final float x;
+ if (isRtl) {
+ x = view.getTranslationX() - distance;
+ } else {
+ x = view.getTranslationX() + distance;
+ }
+ return x;
+ }
+ };
private static final ViewPositionComparator sViewPositionComparator =
new ViewPositionComparator();
@@ -131,9 +134,10 @@ public class FadeAndShortSlide extends Visibility {
getTransitionTargets((ViewGroup) parentForDelay, transitionTargets);
sViewPositionComparator.mParentForDelay = parentForDelay;
sViewPositionComparator.mIsLtr = view.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
- sViewPositionComparator.mToLeft = sViewPositionComparator.mIsLtr
- ? mSlideEdge == (appear ? Gravity.END : Gravity.START)
- : mSlideEdge == (appear ? Gravity.START : Gravity.END);
+ sViewPositionComparator.mToLeft =
+ sViewPositionComparator.mIsLtr
+ ? mSlideEdge == (appear ? Gravity.END : Gravity.START)
+ : mSlideEdge == (appear ? Gravity.START : Gravity.END);
Collections.sort(transitionTargets, sViewPositionComparator);
return transitionTargets.indexOf(view);
}
@@ -180,8 +184,8 @@ public class FadeAndShortSlide extends Visibility {
captureValues(transitionValues);
int delayIndex = getDelayOrder(transitionValues.view, false);
if (delayIndex > 0) {
- transitionValues.values.put(PROPNAME_DELAY,
- delayIndex * SetupAnimationHelper.DELAY_BETWEEN_SIBLINGS_MS);
+ transitionValues.values.put(
+ PROPNAME_DELAY, delayIndex * SetupAnimationHelper.DELAY_BETWEEN_SIBLINGS_MS);
}
}
@@ -192,8 +196,8 @@ public class FadeAndShortSlide extends Visibility {
captureValues(transitionValues);
int delayIndex = getDelayOrder(transitionValues.view, true);
if (delayIndex > 0) {
- transitionValues.values.put(PROPNAME_DELAY,
- delayIndex * SetupAnimationHelper.DELAY_BETWEEN_SIBLINGS_MS);
+ transitionValues.values.put(
+ PROPNAME_DELAY, delayIndex * SetupAnimationHelper.DELAY_BETWEEN_SIBLINGS_MS);
}
}
@@ -212,7 +216,10 @@ public class FadeAndShortSlide extends Visibility {
}
@Override
- public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues,
+ public Animator onAppear(
+ ViewGroup sceneRoot,
+ View view,
+ TransitionValues startValues,
TransitionValues endValues) {
if (endValues == null) {
return null;
@@ -221,15 +228,16 @@ public class FadeAndShortSlide extends Visibility {
int left = position[0];
float endX = view.getTranslationX();
float startX = mSlideCalculator.getGoneX(sceneRoot, view, position, mDistance);
- final Animator slideAnimator = TranslationAnimationCreator.createAnimation(view, endValues,
- left, startX, endX, APPEAR_INTERPOLATOR, this);
+ final Animator slideAnimator =
+ TranslationAnimationCreator.createAnimation(
+ view, endValues, left, startX, endX, APPEAR_INTERPOLATOR, this);
if (slideAnimator == null) {
return null;
}
mFade.setInterpolator(APPEAR_INTERPOLATOR);
final AnimatorSet set = new AnimatorSet();
set.play(slideAnimator).with(mFade.onAppear(sceneRoot, view, startValues, endValues));
- Long delay = (Long ) endValues.values.get(PROPNAME_DELAY);
+ Long delay = (Long) endValues.values.get(PROPNAME_DELAY);
if (delay != null) {
set.setStartDelay(delay);
}
@@ -237,7 +245,10 @@ public class FadeAndShortSlide extends Visibility {
}
@Override
- public Animator onDisappear(ViewGroup sceneRoot, final View view, TransitionValues startValues,
+ public Animator onDisappear(
+ ViewGroup sceneRoot,
+ final View view,
+ TransitionValues startValues,
TransitionValues endValues) {
if (startValues == null) {
return null;
@@ -246,8 +257,9 @@ public class FadeAndShortSlide extends Visibility {
int left = position[0];
float startX = view.getTranslationX();
float endX = mSlideCalculator.getGoneX(sceneRoot, view, position, mDistance);
- final Animator slideAnimator = TranslationAnimationCreator.createAnimation(view,
- startValues, left, startX, endX, DISAPPEAR_INTERPOLATOR, this);
+ final Animator slideAnimator =
+ TranslationAnimationCreator.createAnimation(
+ view, startValues, left, startX, endX, DISAPPEAR_INTERPOLATOR, this);
if (slideAnimator == null) { // slideAnimator is null if startX == endX
return null;
}
@@ -257,13 +269,14 @@ public class FadeAndShortSlide extends Visibility {
if (fadeAnimator == null) {
return null;
}
- fadeAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animator) {
- fadeAnimator.removeListener(this);
- view.setAlpha(0.0f);
- }
- });
+ fadeAnimator.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ fadeAnimator.removeListener(this);
+ view.setAlpha(0.0f);
+ }
+ });
final AnimatorSet set = new AnimatorSet();
set.play(slideAnimator).with(fadeAnimator);
@@ -300,9 +313,7 @@ public class FadeAndShortSlide extends Visibility {
return super.setDuration(scaledDuration);
}
- /**
- * Sets the moving distance in pixel.
- */
+ /** Sets the moving distance in pixel. */
public void setDistance(int distance) {
mDistance = distance;
}
diff --git a/common/src/com/android/tv/common/ui/setup/animation/SetupAnimationHelper.java b/common/src/com/android/tv/common/ui/setup/animation/SetupAnimationHelper.java
index d98138a2..2a913c6e 100644
--- a/common/src/com/android/tv/common/ui/setup/animation/SetupAnimationHelper.java
+++ b/common/src/com/android/tv/common/ui/setup/animation/SetupAnimationHelper.java
@@ -27,12 +27,9 @@ import android.transition.TransitionSet;
import android.view.Gravity;
import android.view.View;
import android.widget.ImageView;
-
import com.android.tv.common.R;
-/**
- * A helper class for setup animation.
- */
+/** A helper class for setup animation. */
public final class SetupAnimationHelper {
public static final long DELAY_BETWEEN_SIBLINGS_MS = applyAnimationTimeScale(33);
@@ -43,21 +40,21 @@ public final class SetupAnimationHelper {
private static int sFragmentTransitionLongDistance;
private static int sFragmentTransitionShortDistance;
- private SetupAnimationHelper() { }
+ private SetupAnimationHelper() {}
- /**
- * Load initial parameters. This method should be called before using this class.
- */
+ /** Load initial parameters. This method should be called before using this class. */
public static void initialize(Context context) {
if (sInitialized) {
return;
}
- sFragmentTransitionDuration = context.getResources()
- .getInteger(R.integer.setup_fragment_transition_duration);
- sFragmentTransitionLongDistance = context.getResources()
- .getDimensionPixelOffset(R.dimen.setup_fragment_transition_long_distance);
- sFragmentTransitionShortDistance = context.getResources()
- .getDimensionPixelOffset(R.dimen.setup_fragment_transition_short_distance);
+ sFragmentTransitionDuration =
+ context.getResources().getInteger(R.integer.setup_fragment_transition_duration);
+ sFragmentTransitionLongDistance =
+ context.getResources()
+ .getDimensionPixelOffset(R.dimen.setup_fragment_transition_long_distance);
+ sFragmentTransitionShortDistance =
+ context.getResources()
+ .getDimensionPixelOffset(R.dimen.setup_fragment_transition_short_distance);
sInitialized = true;
}
@@ -88,9 +85,7 @@ public final class SetupAnimationHelper {
return this;
}
- /**
- * Sets the duration of the transition.
- */
+ /** Sets the duration of the transition. */
public TransitionBuilder setDuration(long duration) {
mDuration = duration;
return this;
@@ -106,17 +101,13 @@ public final class SetupAnimationHelper {
return this;
}
- /**
- * Sets the ID's of the views which will not be included in the transition.
- */
+ /** Sets the ID's of the views which will not be included in the transition. */
public TransitionBuilder setExcludeIds(int[] excludeIds) {
mExcludeIds = excludeIds;
return this;
}
- /**
- * Builds and returns the {@link android.transition.Transition}.
- */
+ /** Builds and returns the {@link android.transition.Transition}. */
public Transition build() {
FadeAndShortSlide transition = new FadeAndShortSlide(mSlideEdge, mParentIdForDelay);
transition.setDistance(mDistance);
@@ -130,25 +121,19 @@ public final class SetupAnimationHelper {
}
}
- /**
- * Changes the move distance of the {@code transition} to long distance.
- */
+ /** Changes the move distance of the {@code transition} to long distance. */
public static void setLongDistance(FadeAndShortSlide transition) {
checkInitialized();
transition.setDistance(sFragmentTransitionLongDistance);
}
- /**
- * Changes the move distance of the {@code transition} to short distance.
- */
+ /** Changes the move distance of the {@code transition} to short distance. */
public static void setShortDistance(FadeAndShortSlide transition) {
checkInitialized();
transition.setDistance(sFragmentTransitionShortDistance);
}
- /**
- * Applies the animation scale to the given {@code animator}.
- */
+ /** Applies the animation scale to the given {@code animator}. */
public static Animator applyAnimationTimeScale(Animator animator) {
if (animator instanceof AnimatorSet) {
for (Animator child : ((AnimatorSet) animator).getChildAnimations()) {
@@ -162,9 +147,7 @@ public final class SetupAnimationHelper {
return animator;
}
- /**
- * Applies the animation scale to the given {@code transition}.
- */
+ /** Applies the animation scale to the given {@code transition}. */
public static Transition applyAnimationTimeScale(Transition transition) {
if (transition instanceof TransitionSet) {
TransitionSet set = (TransitionSet) transition;
@@ -180,9 +163,7 @@ public final class SetupAnimationHelper {
return transition;
}
- /**
- * Applies the animation scale to the given {@code time}.
- */
+ /** Applies the animation scale to the given {@code time}. */
public static long applyAnimationTimeScale(long time) {
return (long) (time * ANIMATION_TIME_SCALE);
}
@@ -197,23 +178,25 @@ public final class SetupAnimationHelper {
}
/**
- * Returns an animator which animates the source image of the {@link ImageView} with start delay.
+ * Returns an animator which animates the source image of the {@link ImageView} with start
+ * delay.
*
* <p>The frame rate is 60 fps.
*/
- public static ObjectAnimator createFrameAnimatorWithDelay(ImageView imageView, int[] frames,
- long startDelay) {
+ public static ObjectAnimator createFrameAnimatorWithDelay(
+ ImageView imageView, int[] frames, long startDelay) {
ObjectAnimator animator = ObjectAnimator.ofInt(imageView, "imageResource", frames);
// Make it 60 fps.
animator.setDuration(frames.length * 1000 / 60);
animator.setInterpolator(null);
animator.setStartDelay(startDelay);
- animator.setEvaluator(new TypeEvaluator<Integer>() {
- @Override
- public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
- return startValue;
- }
- });
+ animator.setEvaluator(
+ new TypeEvaluator<Integer>() {
+ @Override
+ public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
+ return startValue;
+ }
+ });
return animator;
}
@@ -223,19 +206,20 @@ public final class SetupAnimationHelper {
* @param view The view which will be animated.
* @param duration The duration of the animation.
* @param makeVisibleAfterAnimation If {@code true}, the view will become visible after the
- * animation ends.
+ * animation ends.
*/
- public static Animator createFadeOutAnimator(final View view, long duration,
- boolean makeVisibleAfterAnimation) {
+ public static Animator createFadeOutAnimator(
+ final View view, long duration, boolean makeVisibleAfterAnimation) {
ObjectAnimator animator =
ObjectAnimator.ofFloat(view, View.ALPHA, 1.0f, 0.0f).setDuration(duration);
if (makeVisibleAfterAnimation) {
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- view.setAlpha(1.0f);
- }
- });
+ animator.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ view.setAlpha(1.0f);
+ }
+ });
}
return animator;
}
diff --git a/common/src/com/android/tv/common/ui/setup/animation/TranslationAnimationCreator.java b/common/src/com/android/tv/common/ui/setup/animation/TranslationAnimationCreator.java
index 99b8811a..13b89ea1 100644
--- a/common/src/com/android/tv/common/ui/setup/animation/TranslationAnimationCreator.java
+++ b/common/src/com/android/tv/common/ui/setup/animation/TranslationAnimationCreator.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.android.tv.common.ui.setup.animation;
import android.animation.Animator;
@@ -5,18 +20,16 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.graphics.Path;
+import android.support.v17.leanback.R;
import android.transition.Transition;
import android.transition.TransitionValues;
import android.view.View;
-import com.android.tv.common.R;
-
/**
* This class is used by Slide and Explode to create an animator that goes from the start position
* to the end position. It takes into account the canceled position so that it will not blink out or
- * shift suddenly when the transition is interrupted.
- * The original class is android.support.v17.leanback.transition.TranslationAnimationCreator which
- * is hidden.
+ * shift suddenly when the transition is interrupted. The original class is
+ * android.support.v17.leanback.transition.TranslationAnimationCreator which is hidden.
*/
// Copied from android.support.v17.leanback.transition.TransltaionAnimationCreator
class TranslationAnimationCreator {
@@ -31,11 +44,16 @@ class TranslationAnimationCreator {
* @param endX The end translation x of view
* @param interpolator The interpolator to use with this animator.
* @return An animator that moves from (startX, startY) to (endX, endY) unless there was a
- * previous interruption, in which case it moves from the current position to (endX,
- * endY).
+ * previous interruption, in which case it moves from the current position to (endX, endY).
*/
- static Animator createAnimation(View view, TransitionValues values, int viewPosX, float startX,
- float endX, TimeInterpolator interpolator, Transition transition) {
+ static Animator createAnimation(
+ View view,
+ TransitionValues values,
+ int viewPosX,
+ float startX,
+ float endX,
+ TimeInterpolator interpolator,
+ Transition transition) {
float terminalX = view.getTranslationX();
Integer startPosition = (Integer) values.view.getTag(R.id.transitionPosition);
if (startPosition != null) {
@@ -74,8 +92,8 @@ class TranslationAnimationCreator {
private float mPausedX;
private final float mTerminalX;
- private TransitionPositionListener(View movingView, View viewInHierarchy, int startX,
- float terminalX) {
+ private TransitionPositionListener(
+ View movingView, View viewInHierarchy, int startX, float terminalX) {
mMovingView = movingView;
mViewInHierarchy = viewInHierarchy;
mStartX = startX - Math.round(mMovingView.getTranslationX());
@@ -123,6 +141,4 @@ class TranslationAnimationCreator {
@Override
public void onTransitionResume(Transition transition) {}
}
-
}
-
diff --git a/common/src/com/android/tv/common/AutoCloseableUtils.java b/common/src/com/android/tv/common/util/AutoCloseableUtils.java
index ad364cc4..605715ef 100644
--- a/common/src/com/android/tv/common/AutoCloseableUtils.java
+++ b/common/src/com/android/tv/common/util/AutoCloseableUtils.java
@@ -14,17 +14,15 @@
* limitations under the License
*/
-package com.android.tv.common;
+package com.android.tv.common.util;
import android.util.Log;
-/**
- * Static utilities for AutoCloseable.
- */
+/** Static utilities for AutoCloseable. */
public class AutoCloseableUtils {
private static final String TAG = "AutoCloseableUtils";
- private AutoCloseableUtils() { }
+ private AutoCloseableUtils() {}
public static void closeQuietly(AutoCloseable closeable) {
try {
diff --git a/common/src/com/android/tv/common/util/Clock.java b/common/src/com/android/tv/common/util/Clock.java
new file mode 100644
index 00000000..cd6ede86
--- /dev/null
+++ b/common/src/com/android/tv/common/util/Clock.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.util;
+
+import android.os.SystemClock;
+
+/**
+ * An interface through which system clocks can be read. The {@link #SYSTEM} implementation must be
+ * used for all non-test cases.
+ */
+public interface Clock {
+ /**
+ * Returns the current time in milliseconds since January 1, 1970 00:00:00.0 UTC.
+ *
+ * @see System#currentTimeMillis().
+ */
+ long currentTimeMillis();
+
+ /**
+ * Returns milliseconds since boot, including time spent in sleep.
+ *
+ * @see SystemClock#elapsedRealtime()
+ */
+ long elapsedRealtime();
+
+ /**
+ * Returns milliseconds since boot, not counting time spent in deep sleep.
+ *
+ * @return milliseconds of non-sleep uptime since boot.
+ * @see SystemClock#uptimeMillis()
+ */
+ long uptimeMillis();
+
+ /**
+ * Waits a given number of milliseconds (of uptimeMillis) before returning.
+ *
+ * @param ms to sleep before returning, in milliseconds of uptime.
+ * @see SystemClock#sleep(long)
+ */
+ void sleep(long ms);
+
+ /** The default implementation of Clock. */
+ Clock SYSTEM =
+ new Clock() {
+ @Override
+ public long currentTimeMillis() {
+ return System.currentTimeMillis();
+ }
+
+ @Override
+ public long elapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+
+ @Override
+ public void sleep(long ms) {
+ SystemClock.sleep(ms);
+ }
+
+ @Override
+ public long uptimeMillis() {
+ return SystemClock.uptimeMillis();
+ }
+ };
+}
diff --git a/common/src/com/android/tv/common/CollectionUtils.java b/common/src/com/android/tv/common/util/CollectionUtils.java
index 300ad8f2..8ca7e3cc 100644
--- a/common/src/com/android/tv/common/CollectionUtils.java
+++ b/common/src/com/android/tv/common/util/CollectionUtils.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.common;
+package com.android.tv.common.util;
import java.util.ArrayList;
import java.util.Arrays;
@@ -23,16 +23,14 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.List;
-/**
- * Static utilities for collections
- */
+/** Static utilities for collections */
public class CollectionUtils {
/**
* Returns an array with the arrays concatenated together.
*
- * @see <a href="http://stackoverflow.com/a/784842/1122089">Stackoverflow answer</a> by
- * <a href="http://stackoverflow.com/users/40342/joachim-sauer">Joachim Sauer</a>
+ * @see <a href="http://stackoverflow.com/a/784842/1122089">Stackoverflow answer</a> by <a
+ * href="http://stackoverflow.com/users/40342/joachim-sauer">Joachim Sauer</a>
*/
public static <T> T[] concatAll(T[] first, T[]... rest) {
int totalLength = first.length;
@@ -50,12 +48,12 @@ public class CollectionUtils {
/**
* Unions the two collections and returns the unified list.
- * <p>
- * The elements is not compared with hashcode() or equals(). Comparator is used for the equality
- * check.
+ *
+ * <p>The elements is not compared with hashcode() or equals(). Comparator is used for the
+ * equality check.
*/
- public static <T> List<T> union(Collection<T> originals, Collection<T> toAdds,
- Comparator<T> comparator) {
+ public static <T> List<T> union(
+ Collection<T> originals, Collection<T> toAdds, Comparator<T> comparator) {
List<T> result = new ArrayList<>(originals);
Collections.sort(result, comparator);
List<T> resultToAdd = new ArrayList<>();
@@ -68,11 +66,9 @@ public class CollectionUtils {
return result;
}
- /**
- * Subtracts the elements from the original collection.
- */
- public static <T> List<T> subtract(Collection<T> originals, T[] toSubtracts,
- Comparator<T> comparator) {
+ /** Subtracts the elements from the original collection. */
+ public static <T> List<T> subtract(
+ Collection<T> originals, T[] toSubtracts, Comparator<T> comparator) {
List<T> result = new ArrayList<>(originals);
Collections.sort(result, comparator);
for (T toSubtract : toSubtracts) {
@@ -84,11 +80,9 @@ public class CollectionUtils {
return result;
}
- /**
- * Returns {@code true} if the two specified collections have common elements.
- */
- public static <T> boolean containsAny(Collection<T> c1, Collection<T> c2,
- Comparator<T> comparator) {
+ /** Returns {@code true} if the two specified collections have common elements. */
+ public static <T> boolean containsAny(
+ Collection<T> c1, Collection<T> c2, Comparator<T> comparator) {
List<T> contains = new ArrayList<>(c1);
Collections.sort(contains, comparator);
for (T iterate : c2) {
diff --git a/common/src/com/android/tv/common/util/CommonUtils.java b/common/src/com/android/tv/common/util/CommonUtils.java
new file mode 100644
index 00000000..305431d3
--- /dev/null
+++ b/common/src/com/android/tv/common/util/CommonUtils.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.common.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.media.tv.TvInputInfo;
+import android.os.Build;
+import android.util.ArraySet;
+import android.util.Log;
+import com.android.tv.common.BuildConfig;
+import com.android.tv.common.CommonConstants;
+import com.android.tv.common.actions.InputSetupActionUtils;
+import com.android.tv.common.experiments.Experiments;
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Set;
+
+/** Util class for common use in TV app and inputs. */
+@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated
+public final class CommonUtils {
+ private static final String TAG = "CommonUtils";
+ private static final ThreadLocal<SimpleDateFormat> ISO_8601 =
+ new ThreadLocal() {
+ private final SimpleDateFormat value =
+ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);
+
+ @Override
+ protected SimpleDateFormat initialValue() {
+ return value;
+ }
+ };
+ // Hardcoded list for known bundled inputs not written by OEM/SOCs.
+ // Bundled (system) inputs not in the list will get the high priority
+ // so they and their channels come first in the UI.
+ private static final Set<String> BUNDLED_PACKAGE_SET = new ArraySet<>();
+
+ static {
+ BUNDLED_PACKAGE_SET.add("com.android.tv");
+ }
+
+ private static Boolean sRunningInTest;
+
+ private CommonUtils() {}
+
+ /**
+ * Returns an intent to start the setup activity for the TV input using {@link
+ * InputSetupActionUtils#INTENT_ACTION_INPUT_SETUP}.
+ */
+ public static Intent createSetupIntent(Intent originalSetupIntent, String inputId) {
+ if (originalSetupIntent == null) {
+ return null;
+ }
+ Intent setupIntent = new Intent(originalSetupIntent);
+ if (!InputSetupActionUtils.hasInputSetupAction(originalSetupIntent)) {
+ Intent intentContainer = new Intent(InputSetupActionUtils.INTENT_ACTION_INPUT_SETUP);
+ intentContainer.putExtra(InputSetupActionUtils.EXTRA_SETUP_INTENT, originalSetupIntent);
+ intentContainer.putExtra(InputSetupActionUtils.EXTRA_INPUT_ID, inputId);
+ setupIntent = intentContainer;
+ }
+ return setupIntent;
+ }
+
+ /**
+ * Returns an intent to start the setup activity for this TV input using {@link
+ * InputSetupActionUtils#INTENT_ACTION_INPUT_SETUP}.
+ */
+ public static Intent createSetupIntent(TvInputInfo input) {
+ return createSetupIntent(input.createSetupIntent(), input.getId());
+ }
+
+ /**
+ * Checks if this application is running in tests.
+ *
+ * <p>{@link android.app.ActivityManager#isRunningInTestHarness} doesn't return {@code true} for
+ * the usual devices even the application is running in tests. We need to figure it out by
+ * checking whether the class in tv-tests-common module can be loaded or not.
+ */
+ public static synchronized boolean isRunningInTest() {
+ if (sRunningInTest == null) {
+ try {
+ Class.forName("com.android.tv.testing.utils.Utils");
+ Log.i(
+ TAG,
+ "Assumed to be running in a test because"
+ + " com.android.tv.testing.utils.Utils is found");
+ sRunningInTest = true;
+ } catch (ClassNotFoundException e) {
+ sRunningInTest = false;
+ }
+ }
+ return sRunningInTest;
+ }
+
+ /** Checks whether a given package is in our bundled package set. */
+ public static boolean isInBundledPackageSet(String packageName) {
+ return BUNDLED_PACKAGE_SET.contains(packageName);
+ }
+
+ /** Checks whether a given input is a bundled input. */
+ public static boolean isBundledInput(String inputId) {
+ for (String prefix : BUNDLED_PACKAGE_SET) {
+ if (inputId.startsWith(prefix + "/")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Returns true if the application is packaged with Live TV. */
+ public static boolean isPackagedWithLiveChannels(Context context) {
+ return (CommonConstants.BASE_PACKAGE.equals(context.getPackageName()));
+ }
+
+ /** Returns true if the current user is a developer. */
+ public static boolean isDeveloper() {
+ return BuildConfig.ENG || Experiments.ENABLE_DEVELOPER_FEATURES.get();
+ }
+
+ /** Converts time in milliseconds to a ISO 8061 string. */
+ public static String toIsoDateTimeString(long timeMillis) {
+ return ISO_8601.get().format(new Date(timeMillis));
+ }
+
+ /** Deletes a file or a directory. */
+ public static void deleteDirOrFile(File fileOrDirectory) {
+ if (fileOrDirectory.isDirectory()) {
+ for (File child : fileOrDirectory.listFiles()) {
+ deleteDirOrFile(child);
+ }
+ }
+ fileOrDirectory.delete();
+ }
+
+ public static boolean isRoboTest() {
+ return "robolectric".equals(Build.FINGERPRINT);
+ }
+}
diff --git a/common/src/com/android/tv/common/util/ContentUriUtils.java b/common/src/com/android/tv/common/util/ContentUriUtils.java
new file mode 100644
index 00000000..6cbe5e12
--- /dev/null
+++ b/common/src/com/android/tv/common/util/ContentUriUtils.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.common.util;
+
+import android.content.ContentUris;
+import android.net.Uri;
+import android.util.Log;
+
+/** Static utils for{@link android.content.ContentUris}. */
+public class ContentUriUtils {
+ private static final String TAG = "ContentUriUtils";
+
+ /**
+ * Converts the last path segment to a long.
+ *
+ * <p>This supports a common convention for content URIs where an ID is stored in the last
+ * segment.
+ *
+ * @return the long conversion of the last segment or -1 if the path is empty or there is any
+ * error
+ * @see ContentUris#parseId(Uri)
+ */
+ public static long safeParseId(Uri uri) {
+ try {
+ return ContentUris.parseId(uri);
+ } catch (Exception e) {
+ Log.d(TAG, "Error parsing " + uri, e);
+ return -1;
+ }
+ }
+}
diff --git a/common/src/com/android/tv/common/util/Debug.java b/common/src/com/android/tv/common/util/Debug.java
new file mode 100644
index 00000000..ab908741
--- /dev/null
+++ b/common/src/com/android/tv/common/util/Debug.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.common.util;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/** A class only for help developers. */
+public class Debug {
+ /**
+ * A threshold of start up time, when the start up time of Live TV is more than it, a
+ * warning will show to the developer.
+ */
+ public static final long TIME_START_UP_DURATION_THRESHOLD = TimeUnit.SECONDS.toMillis(6);
+ /** Tag for measuring start up time of Live TV. */
+ public static final String TAG_START_UP_TIMER = "start_up_timer";
+
+ /** A global map for duration timers. */
+ private static final Map<String, DurationTimer> sTimerMap = new HashMap<>();
+
+ /** Returns the global duration timer by tag. */
+ public static DurationTimer getTimer(String tag) {
+ if (sTimerMap.get(tag) != null) {
+ return sTimerMap.get(tag);
+ }
+ DurationTimer timer = new DurationTimer(tag, true);
+ sTimerMap.put(tag, timer);
+ return timer;
+ }
+
+ /** Removes the global duration timer by tag. */
+ public static DurationTimer removeTimer(String tag) {
+ return sTimerMap.remove(tag);
+ }
+}
diff --git a/common/src/com/android/tv/common/util/DurationTimer.java b/common/src/com/android/tv/common/util/DurationTimer.java
new file mode 100644
index 00000000..91581ad5
--- /dev/null
+++ b/common/src/com/android/tv/common/util/DurationTimer.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.common.util;
+
+import android.os.SystemClock;
+import android.util.Log;
+import com.android.tv.common.BuildConfig;
+
+/** Times a duration. */
+public final class DurationTimer {
+ private static final String TAG = "DurationTimer";
+ public static final long TIME_NOT_SET = -1;
+
+ private long mStartTimeMs = TIME_NOT_SET;
+ private String mTag = TAG;
+ private boolean mLogEngOnly;
+
+ public DurationTimer() {}
+
+ public DurationTimer(String tag, boolean logEngOnly) {
+ mTag = tag;
+ mLogEngOnly = logEngOnly;
+ }
+
+ /** Returns true if the timer is running. */
+ public boolean isRunning() {
+ return mStartTimeMs != TIME_NOT_SET;
+ }
+
+ /** Start the timer. */
+ public void start() {
+ mStartTimeMs = SystemClock.elapsedRealtime();
+ }
+
+ /** Returns true if timer is started. */
+ public boolean isStarted() {
+ return mStartTimeMs != TIME_NOT_SET;
+ }
+
+ /**
+ * Returns the current duration in milliseconds or {@link #TIME_NOT_SET} if the timer is not
+ * running.
+ */
+ public long getDuration() {
+ return isRunning() ? SystemClock.elapsedRealtime() - mStartTimeMs : TIME_NOT_SET;
+ }
+
+ /**
+ * Stops the timer and resets its value to {@link #TIME_NOT_SET}.
+ *
+ * @return the current duration in milliseconds or {@link #TIME_NOT_SET} if the timer is not
+ * running.
+ */
+ public long reset() {
+ long duration = getDuration();
+ mStartTimeMs = TIME_NOT_SET;
+ return duration;
+ }
+
+ /** Adds information and duration time to the log. */
+ public void log(String message) {
+ if (isRunning() && (!mLogEngOnly || BuildConfig.ENG)) {
+ Log.i(mTag, message + " : " + getDuration() + "ms");
+ }
+ }
+}
diff --git a/common/src/com/android/tv/common/util/LocationUtils.java b/common/src/com/android/tv/common/util/LocationUtils.java
new file mode 100644
index 00000000..53155298
--- /dev/null
+++ b/common/src/com/android/tv/common/util/LocationUtils.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.common.util;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.location.Address;
+import android.location.Geocoder;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.tv.common.BuildConfig;
+
+
+
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+
+/** A utility class to get the current location. */
+public class LocationUtils {
+ private static final String TAG = "LocationUtils";
+ private static final boolean DEBUG = false;
+
+ private static Context sApplicationContext;
+ private static Address sAddress;
+ private static String sCountry;
+ private static IOException sError;
+
+ /** Checks the current location. */
+ public static synchronized Address getCurrentAddress(Context context)
+ throws IOException, SecurityException {
+ if (sAddress != null) {
+ return sAddress;
+ }
+ if (sError != null) {
+ throw sError;
+ }
+ if (sApplicationContext == null) {
+ sApplicationContext = context.getApplicationContext();
+ }
+ LocationUtilsHelper.startLocationUpdates();
+ return null;
+ }
+
+ /** Returns the current country. */
+ @NonNull
+ public static synchronized String getCurrentCountry(Context context) {
+ if (sCountry != null) {
+ return sCountry;
+ }
+ if (TextUtils.isEmpty(sCountry)) {
+ sCountry = context.getResources().getConfiguration().locale.getCountry();
+ }
+ return sCountry;
+ }
+
+ private static void updateAddress(Location location) {
+ if (DEBUG) Log.d(TAG, "Updating address with " + location);
+ if (location == null) {
+ return;
+ }
+ Geocoder geocoder = new Geocoder(sApplicationContext, Locale.getDefault());
+ try {
+ List<Address> addresses =
+ geocoder.getFromLocation(location.getLatitude(), location.getLongitude(), 1);
+ if (addresses != null && !addresses.isEmpty()) {
+ sAddress = addresses.get(0);
+ if (DEBUG) Log.d(TAG, "Got " + sAddress);
+ try {
+ PostalCodeUtils.updatePostalCode(sApplicationContext);
+ } catch (Exception e) {
+ // Do nothing
+ }
+ } else {
+ if (DEBUG) Log.d(TAG, "No address returned");
+ }
+ sError = null;
+ } catch (IOException e) {
+ Log.w(TAG, "Error in updating address", e);
+ sError = e;
+ }
+ }
+
+ private LocationUtils() {}
+
+ private static class LocationUtilsHelper {
+ private static final LocationListener LOCATION_LISTENER =
+ new LocationListener() {
+ @Override
+ public void onLocationChanged(Location location) {
+ updateAddress(location);
+ }
+
+ @Override
+ public void onStatusChanged(String provider, int status, Bundle extras) {}
+
+ @Override
+ public void onProviderEnabled(String provider) {}
+
+ @Override
+ public void onProviderDisabled(String provider) {}
+ };
+
+ private static LocationManager sLocationManager;
+
+ public static void startLocationUpdates() {
+ if (sLocationManager == null) {
+ sLocationManager =
+ (LocationManager)
+ sApplicationContext.getSystemService(Context.LOCATION_SERVICE);
+ try {
+ sLocationManager.requestLocationUpdates(
+ LocationManager.NETWORK_PROVIDER, 1000, 10, LOCATION_LISTENER, null);
+ } catch (SecurityException e) {
+ // Enables requesting the location updates again.
+ sLocationManager = null;
+ throw e;
+ }
+ }
+ }
+ }
+}
diff --git a/common/src/com/android/tv/common/util/NetworkTrafficTags.java b/common/src/com/android/tv/common/util/NetworkTrafficTags.java
new file mode 100644
index 00000000..91f2bcd1
--- /dev/null
+++ b/common/src/com/android/tv/common/util/NetworkTrafficTags.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.common.util;
+
+import android.net.TrafficStats;
+import android.support.annotation.NonNull;
+import java.util.concurrent.Executor;
+
+/** Constants for tagging network traffic in the Live channels app. */
+public final class NetworkTrafficTags {
+
+ public static final int DEFAULT_LIVE_CHANNELS = 1;
+ public static final int LOGO_FETCHER = 2;
+ public static final int HDHOMERUN = 3;
+ public static final int EPG_FETCH = 4;
+
+ /**
+ * An executor which simply wraps a provided delegate executor, but calls {@link
+ * TrafficStats#setThreadStatsTag(int)} before executing any task.
+ */
+ public static class TrafficStatsTaggingExecutor implements Executor {
+ private final Executor delegateExecutor;
+ private final int tag;
+
+ public TrafficStatsTaggingExecutor(Executor delegateExecutor, int tag) {
+ this.delegateExecutor = delegateExecutor;
+ this.tag = tag;
+ }
+
+ @Override
+ public void execute(final @NonNull Runnable command) {
+ // TODO(b/62038127): robolectric does not support lamdas in unbundled apps
+ delegateExecutor.execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ TrafficStats.setThreadStatsTag(tag);
+ try {
+ command.run();
+ } finally {
+ TrafficStats.clearThreadStatsTag();
+ }
+ }
+ });
+ }
+ }
+
+ private NetworkTrafficTags() {}
+}
diff --git a/common/src/com/android/tv/common/util/PermissionUtils.java b/common/src/com/android/tv/common/util/PermissionUtils.java
new file mode 100644
index 00000000..8d409e50
--- /dev/null
+++ b/common/src/com/android/tv/common/util/PermissionUtils.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.util;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+/** Util class to handle permissions. */
+public class PermissionUtils {
+ /** Permission to read the TV listings. */
+ public static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";
+
+ private static Boolean sHasAccessAllEpgPermission;
+ private static Boolean sHasAccessWatchedHistoryPermission;
+ private static Boolean sHasModifyParentalControlsPermission;
+
+ public static boolean hasAccessAllEpg(Context context) {
+ if (sHasAccessAllEpgPermission == null) {
+ sHasAccessAllEpgPermission =
+ context.checkSelfPermission(
+ "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA")
+ == PackageManager.PERMISSION_GRANTED;
+ }
+ return sHasAccessAllEpgPermission;
+ }
+
+ public static boolean hasAccessWatchedHistory(Context context) {
+ if (sHasAccessWatchedHistoryPermission == null) {
+ sHasAccessWatchedHistoryPermission =
+ context.checkSelfPermission(
+ "com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS")
+ == PackageManager.PERMISSION_GRANTED;
+ }
+ return sHasAccessWatchedHistoryPermission;
+ }
+
+ public static boolean hasModifyParentalControls(Context context) {
+ if (sHasModifyParentalControlsPermission == null) {
+ sHasModifyParentalControlsPermission =
+ context.checkSelfPermission("android.permission.MODIFY_PARENTAL_CONTROLS")
+ == PackageManager.PERMISSION_GRANTED;
+ }
+ return sHasModifyParentalControlsPermission;
+ }
+
+ public static boolean hasReadTvListings(Context context) {
+ return context.checkSelfPermission(PERMISSION_READ_TV_LISTINGS)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ public static boolean hasInternet(Context context) {
+ return context.checkSelfPermission("android.permission.INTERNET")
+ == PackageManager.PERMISSION_GRANTED;
+ }
+}
diff --git a/common/src/com/android/tv/common/util/PostalCodeUtils.java b/common/src/com/android/tv/common/util/PostalCodeUtils.java
new file mode 100644
index 00000000..c0917af2
--- /dev/null
+++ b/common/src/com/android/tv/common/util/PostalCodeUtils.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.common.util;
+
+import android.content.Context;
+import android.location.Address;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.tv.common.CommonPreferences;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/** A utility class to update, get, and set the last known postal or zip code. */
+public class PostalCodeUtils {
+ private static final String TAG = "PostalCodeUtils";
+
+ // Postcode formats, where A signifies a letter and 9 a digit:
+ // US zip code format: 99999
+ private static final String POSTCODE_REGEX_US = "^(\\d{5})";
+ // UK postcode district formats: A9, A99, AA9, AA99
+ // Full UK postcode format: Postcode District + space + 9AA
+ // Should be able to handle both postcode district and full postcode
+ private static final String POSTCODE_REGEX_GB =
+ "^([A-Z][A-Z]?[0-9][0-9A-Z]?)( ?[0-9][A-Z]{2})?$";
+ private static final String POSTCODE_REGEX_GB_GIR = "^GIR( ?0AA)?$"; // special UK postcode
+
+ private static final Map<String, Pattern> REGION_PATTERN = new HashMap<>();
+ private static final Map<String, Integer> REGION_MAX_LENGTH = new HashMap<>();
+
+ static {
+ REGION_PATTERN.put(Locale.US.getCountry(), Pattern.compile(POSTCODE_REGEX_US));
+ REGION_PATTERN.put(
+ Locale.UK.getCountry(),
+ Pattern.compile(POSTCODE_REGEX_GB + "|" + POSTCODE_REGEX_GB_GIR));
+ REGION_MAX_LENGTH.put(Locale.US.getCountry(), 5);
+ REGION_MAX_LENGTH.put(Locale.UK.getCountry(), 8);
+ }
+
+ // The longest postcode number is 10-character-long.
+ // Use a larger number to accommodate future changes.
+ private static final int DEFAULT_MAX_LENGTH = 16;
+
+ /** Returns {@code true} if postal code has been changed */
+ public static boolean updatePostalCode(Context context)
+ throws IOException, SecurityException, NoPostalCodeException {
+ String postalCode = getPostalCode(context);
+ String lastPostalCode = getLastPostalCode(context);
+ if (TextUtils.isEmpty(postalCode)) {
+ if (TextUtils.isEmpty(lastPostalCode)) {
+ throw new NoPostalCodeException();
+ }
+ } else if (!TextUtils.equals(postalCode, lastPostalCode)) {
+ setLastPostalCode(context, postalCode);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Gets the last stored postal or zip code, which might be decided by {@link LocationUtils} or
+ * input by users.
+ */
+ public static String getLastPostalCode(Context context) {
+ return CommonPreferences.getLastPostalCode(context);
+ }
+
+ /**
+ * Sets the last stored postal or zip code. This method will overwrite the value written by
+ * calling {@link #updatePostalCode(Context)}.
+ */
+ public static void setLastPostalCode(Context context, String postalCode) {
+ Log.i(TAG, "Set Postal Code:" + postalCode);
+ CommonPreferences.setLastPostalCode(context, postalCode);
+ }
+
+ @Nullable
+ private static String getPostalCode(Context context) throws IOException, SecurityException {
+ Address address = LocationUtils.getCurrentAddress(context);
+ if (address != null) {
+ Log.i(
+ TAG,
+ "Current country and postal code is "
+ + address.getCountryName()
+ + ", "
+ + address.getPostalCode());
+ return address.getPostalCode();
+ }
+ return null;
+ }
+
+ /** An {@link java.lang.Exception} class to notify no valid postal or zip code is available. */
+ public static class NoPostalCodeException extends Exception {
+ public NoPostalCodeException() {}
+ }
+
+ /**
+ * Checks whether a postcode matches the format of the specific region.
+ *
+ * @return {@code false} if the region is supported and the postcode doesn't match; {@code true}
+ * otherwise
+ */
+ public static boolean matches(@NonNull CharSequence postcode, @NonNull String region) {
+ Pattern pattern = REGION_PATTERN.get(region.toUpperCase());
+ return pattern == null || pattern.matcher(postcode).matches();
+ }
+
+ /**
+ * Gets the largest possible postcode length in the region.
+ *
+ * @return maximum postcode length if the region is supported; {@link #DEFAULT_MAX_LENGTH}
+ * otherwise
+ */
+ public static int getRegionMaxLength(Context context) {
+ Integer maxLength =
+ REGION_MAX_LENGTH.get(LocationUtils.getCurrentCountry(context).toUpperCase());
+ return maxLength == null ? DEFAULT_MAX_LENGTH : maxLength;
+ }
+}
diff --git a/common/src/com/android/tv/common/SharedPreferencesUtils.java b/common/src/com/android/tv/common/util/SharedPreferencesUtils.java
index 140c4e6f..e8bfe61b 100644
--- a/common/src/com/android/tv/common/SharedPreferencesUtils.java
+++ b/common/src/com/android/tv/common/util/SharedPreferencesUtils.java
@@ -14,15 +14,14 @@
* limitations under the License
*/
-package com.android.tv.common;
+package com.android.tv.common.util;
import android.content.Context;
import android.os.AsyncTask;
import android.preference.PreferenceManager;
+import com.android.tv.common.CommonConstants;
-/**
- * Static utilities for {@link android.content.SharedPreferences}
- */
+/** Static utilities for {@link android.content.SharedPreferences} */
public final class SharedPreferencesUtils {
// Note that changing the preference name will reset the preference values.
public static final String SHARED_PREF_FEATURES = "sharePreferencesFeatures";
@@ -31,7 +30,7 @@ public final class SharedPreferencesUtils {
public static final String SHARED_PREF_DVR_WATCHED_POSITION =
"dvr_watched_position_shared_preference";
public static final String SHARED_PREF_AUDIO_CAPABILITIES =
- "com.android.tv.audio_capabilities";
+ CommonConstants.BASE_PACKAGE + ".audio_capabilities";
public static final String SHARED_PREF_RECURRING_RUNNER = "sharedPreferencesRecurringRunner";
public static final String SHARED_PREF_EPG = "epg_preferences";
public static final String SHARED_PREF_SERIES_RECORDINGS = "seriesRecordings";
@@ -39,15 +38,16 @@ public final class SharedPreferencesUtils {
public static final String SHARED_PREF_CHANNEL_LOGO_URIS = "channelLogoUris";
/** Stores the UI related settings */
public static final String SHARED_PREF_UI_SETTINGS = "ui_settings";
+
public static final String SHARED_PREF_PREVIEW_PROGRAMS = "previewPrograms";
private static boolean sInitializeCalled;
/**
- * {@link android.content.SharedPreferences} loads the preference file when
- * {@link Context#getSharedPreferences(String, int)} is called for the first time.
- * Call {@link Context#getSharedPreferences(String, int)} as early as possible to avoid the ANR
- * due to the file loading.
+ * {@link android.content.SharedPreferences} loads the preference file when {@link
+ * Context#getSharedPreferences(String, int)} is called for the first time. Call {@link
+ * Context#getSharedPreferences(String, int)} as early as possible to avoid the ANR due to the
+ * file loading.
*/
public static synchronized void initialize(final Context context, final Runnable postTask) {
if (!sInitializeCalled) {
@@ -59,15 +59,15 @@ public final class SharedPreferencesUtils {
context.getSharedPreferences(SHARED_PREF_FEATURES, Context.MODE_PRIVATE);
context.getSharedPreferences(SHARED_PREF_BROWSABLE, Context.MODE_PRIVATE);
context.getSharedPreferences(SHARED_PREF_WATCHED_HISTORY, Context.MODE_PRIVATE);
- context.getSharedPreferences(SHARED_PREF_DVR_WATCHED_POSITION,
- Context.MODE_PRIVATE);
- context.getSharedPreferences(SHARED_PREF_AUDIO_CAPABILITIES,
- Context.MODE_PRIVATE);
- context.getSharedPreferences(SHARED_PREF_RECURRING_RUNNER,
- Context.MODE_PRIVATE);
+ context.getSharedPreferences(
+ SHARED_PREF_DVR_WATCHED_POSITION, Context.MODE_PRIVATE);
+ context.getSharedPreferences(
+ SHARED_PREF_AUDIO_CAPABILITIES, Context.MODE_PRIVATE);
+ context.getSharedPreferences(
+ SHARED_PREF_RECURRING_RUNNER, Context.MODE_PRIVATE);
context.getSharedPreferences(SHARED_PREF_EPG, Context.MODE_PRIVATE);
- context.getSharedPreferences(SHARED_PREF_SERIES_RECORDINGS,
- Context.MODE_PRIVATE);
+ context.getSharedPreferences(
+ SHARED_PREF_SERIES_RECORDINGS, Context.MODE_PRIVATE);
context.getSharedPreferences(SHARED_PREF_UI_SETTINGS, Context.MODE_PRIVATE);
return null;
}
@@ -80,5 +80,5 @@ public final class SharedPreferencesUtils {
}
}
- private SharedPreferencesUtils() { }
+ private SharedPreferencesUtils() {}
}
diff --git a/common/src/com/android/tv/common/util/StringUtils.java b/common/src/com/android/tv/common/util/StringUtils.java
new file mode 100644
index 00000000..b9461426
--- /dev/null
+++ b/common/src/com/android/tv/common/util/StringUtils.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.common.util;
+
+/** Utility class for handling {@link String}. */
+public final class StringUtils {
+
+ private StringUtils() {}
+
+ /** Returns compares two strings lexicographically and handles null values quietly. */
+ public static int compare(String a, String b) {
+ if (a == null) {
+ return b == null ? 0 : -1;
+ }
+ if (b == null) {
+ return 1;
+ }
+ return a.compareTo(b);
+ }
+}
diff --git a/common/src/com/android/tv/common/util/SystemProperties.java b/common/src/com/android/tv/common/util/SystemProperties.java
new file mode 100644
index 00000000..a9f18d4b
--- /dev/null
+++ b/common/src/com/android/tv/common/util/SystemProperties.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.common.util;
+
+import com.android.tv.common.BooleanSystemProperty;
+
+/** A convenience class for getting TV related system properties. */
+public final class SystemProperties {
+
+ /** Allow Google Analytics for eng builds. */
+ public static final BooleanSystemProperty ALLOW_ANALYTICS_IN_ENG =
+ new BooleanSystemProperty("tv_allow_analytics_in_eng", false);
+
+ /** Allow Strict mode for debug builds. */
+ public static final BooleanSystemProperty ALLOW_STRICT_MODE =
+ new BooleanSystemProperty("tv_allow_strict_mode", true);
+
+ /** When true {@link android.view.KeyEvent}s are logged. Defaults to false. */
+ public static final BooleanSystemProperty LOG_KEYEVENT =
+ new BooleanSystemProperty("tv_log_keyevent", false);
+ /** When true debug keys are used. Defaults to false. */
+ public static final BooleanSystemProperty USE_DEBUG_KEYS =
+ new BooleanSystemProperty("tv_use_debug_keys", false);
+
+ /** Send {@link com.android.tv.analytics.Tracker} information. Defaults to {@code true}. */
+ public static final BooleanSystemProperty USE_TRACKER =
+ new BooleanSystemProperty("tv_use_tracker", true);
+
+ static {
+ updateSystemProperties();
+ }
+
+ private SystemProperties() {}
+
+ /** Update the TV related system properties. */
+ public static void updateSystemProperties() {
+ BooleanSystemProperty.resetAll();
+ }
+}
diff --git a/common/src/com/android/tv/common/util/SystemPropertiesProxy.java b/common/src/com/android/tv/common/util/SystemPropertiesProxy.java
new file mode 100644
index 00000000..a3ffd0fa
--- /dev/null
+++ b/common/src/com/android/tv/common/util/SystemPropertiesProxy.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.common.util;
+
+import android.util.Log;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Proxy class that gives an access to a hidden API {@link android.os.SystemProperties#getBoolean}.
+ */
+public class SystemPropertiesProxy {
+ private static final String TAG = "SystemPropertiesProxy";
+
+ private SystemPropertiesProxy() {}
+
+ public static boolean getBoolean(String key, boolean def) throws IllegalArgumentException {
+ try {
+ Class SystemPropertiesClass = Class.forName("android.os.SystemProperties");
+ Method getBooleanMethod =
+ SystemPropertiesClass.getDeclaredMethod(
+ "getBoolean", String.class, boolean.class);
+ getBooleanMethod.setAccessible(true);
+ return (boolean) getBooleanMethod.invoke(SystemPropertiesClass, key, def);
+ } catch (InvocationTargetException
+ | IllegalAccessException
+ | NoSuchMethodException
+ | ClassNotFoundException e) {
+ Log.e(TAG, "Failed to invoke SystemProperties.getBoolean()", e);
+ }
+ return def;
+ }
+
+ public static int getInt(String key, int def) throws IllegalArgumentException {
+ try {
+ Class SystemPropertiesClass = Class.forName("android.os.SystemProperties");
+ Method getIntMethod =
+ SystemPropertiesClass.getDeclaredMethod("getInt", String.class, int.class);
+ getIntMethod.setAccessible(true);
+ return (int) getIntMethod.invoke(SystemPropertiesClass, key, def);
+ } catch (InvocationTargetException
+ | IllegalAccessException
+ | NoSuchMethodException
+ | ClassNotFoundException e) {
+ Log.e(TAG, "Failed to invoke SystemProperties.getInt()", e);
+ }
+ return def;
+ }
+
+ public static String getString(String key, String def) throws IllegalArgumentException {
+ try {
+ Class SystemPropertiesClass = Class.forName("android.os.SystemProperties");
+ Method getIntMethod =
+ SystemPropertiesClass.getDeclaredMethod("get", String.class, String.class);
+ getIntMethod.setAccessible(true);
+ return (String) getIntMethod.invoke(SystemPropertiesClass, key, def);
+ } catch (InvocationTargetException
+ | IllegalAccessException
+ | NoSuchMethodException
+ | ClassNotFoundException e) {
+ Log.e(TAG, "Failed to invoke SystemProperties.get()", e);
+ }
+ return def;
+ }
+}