Merge "Avoid NPE in genre details"
diff --git a/build.gradle b/build.gradle
index 52c68db..cf219d4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -20,7 +20,7 @@
         google()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.3.1'
+        classpath 'com.android.tools.build:gradle:3.3.2'
     }
 }
 
diff --git a/java/com/android/pump/activity/PlaylistDetailsActivity.java b/java/com/android/pump/activity/PlaylistDetailsActivity.java
index 0900023..9997a57 100644
--- a/java/com/android/pump/activity/PlaylistDetailsActivity.java
+++ b/java/com/android/pump/activity/PlaylistDetailsActivity.java
@@ -149,18 +149,12 @@
 
         // TODO Find a better way to handle 2x2 art
         Set<Uri> albumArtUris = new HashSet<>();
-        Set<String> artistNames = new HashSet<>();
         List<Audio> audios = mPlaylist.getAudios();
         for (Audio audio : audios) {
             Album album = audio.getAlbum();
             if (album != null && album.getAlbumArtUri() != null) {
                 albumArtUris.add(album.getAlbumArtUri());
             }
-
-            Artist artist = audio.getArtist();
-            if (artist != null && artist.getName() != null) {
-                artistNames.add(artist.getName());
-            }
         }
 
         int numAlbumArt = albumArtUris.size();
diff --git a/java/com/android/pump/activity/PumpActivity.java b/java/com/android/pump/activity/PumpActivity.java
index fd9a910..520a829 100644
--- a/java/com/android/pump/activity/PumpActivity.java
+++ b/java/com/android/pump/activity/PumpActivity.java
@@ -16,7 +16,7 @@
 
 package com.android.pump.activity;
 
-import android.content.pm.PackageManager;
+import android.app.Activity;
 import android.os.Bundle;
 import android.view.Menu;
 import android.view.MenuItem;
@@ -28,8 +28,6 @@
 import androidx.annotation.UiThread;
 import androidx.appcompat.app.ActionBar;
 import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
 import androidx.core.view.GravityCompat;
 import androidx.drawerlayout.widget.DrawerLayout;
 import androidx.fragment.app.Fragment;
@@ -45,9 +43,11 @@
 import com.android.pump.fragment.HomeFragment;
 import com.android.pump.fragment.MovieFragment;
 import com.android.pump.fragment.OtherFragment;
+import com.android.pump.fragment.PermissionFragment;
 import com.android.pump.fragment.PlaylistFragment;
 import com.android.pump.fragment.SeriesFragment;
 import com.android.pump.util.Globals;
+import com.android.pump.util.Permissions;
 import com.google.android.material.bottomnavigation.BottomNavigationView;
 import com.google.android.material.bottomnavigation.BottomNavigationView.OnNavigationItemSelectedListener;
 import com.google.android.material.tabs.TabLayout;
@@ -55,15 +55,13 @@
 @UiThread
 public class PumpActivity extends AppCompatActivity implements OnNavigationItemSelectedListener {
     private static final int REQUIRED_PERMISSIONS_REQUEST_CODE = 42;
-    private static final String[] REQUIRED_PERMISSIONS = {
-        android.Manifest.permission.INTERNET,
-        android.Manifest.permission.READ_EXTERNAL_STORAGE,
-        android.Manifest.permission.WRITE_EXTERNAL_STORAGE
-    };
+
+    // TODO The following should be a non-static member
+    private static boolean sIsMissingPermissions = true;
 
     private static final Pages[] PAGES_LIST = {
         new Pages(R.id.menu_home, new Page[] {
-            new Page(HomeFragment::newInstance, "Home")
+            new PermissionPage(HomeFragment::newInstance, "Home")
         }),
         new Pages(R.id.menu_video, new Page[] {
             new Page(MovieFragment::newInstance, "Movies"),
@@ -91,6 +89,10 @@
     private TabLayout mTabLayout;
     private BottomNavigationView mBottomNavigationView;
 
+    public static void requestPermissions(@NonNull Activity activity) {
+        Permissions.requestMissingPermissions(activity, REQUIRED_PERMISSIONS_REQUEST_CODE);
+    }
+
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -111,7 +113,7 @@
         mViewPager.setAdapter(mActivityPagerAdapter);
         mTabLayout.setupWithViewPager(mViewPager);
 
-        if (!requestMissingPermissions()) {
+        if (!Permissions.isMissingPermissions(this)) {
             initialize();
         }
     }
@@ -134,7 +136,7 @@
     @Override
     public boolean onNavigationItemSelected(@NonNull MenuItem item) {
         for (Pages pages : PAGES_LIST) {
-            if (pages.id == item.getItemId()) {
+            if (pages.getId() == item.getItemId()) {
                 selectPages(item.getTitle(), pages);
                 return true;
             }
@@ -146,19 +148,7 @@
     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
             @NonNull int[] grantResults) {
         if (requestCode == REQUIRED_PERMISSIONS_REQUEST_CODE) {
-            boolean granted = true;
-            if (grantResults.length == 0) {
-                granted = false;
-            } else {
-                for (int grantResult : grantResults) {
-                    if (grantResult != PackageManager.PERMISSION_GRANTED) {
-                        granted = false;
-                    }
-                }
-            }
-            if (!granted) {
-                finish();
-            } else {
+            if (Permissions.isGranted(permissions, grantResults)) {
                 initialize();
             }
         } else {
@@ -167,28 +157,11 @@
     }
 
     private void initialize() {
+        sIsMissingPermissions = false;
+        mActivityPagerAdapter.notifyDataSetChanged();
         Globals.getMediaDb(this).load();
     }
 
-    private boolean requestMissingPermissions() {
-        if (isMissingPermissions()) {
-            ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS,
-                    REQUIRED_PERMISSIONS_REQUEST_CODE);
-            return true;
-        }
-        return false;
-    }
-
-    private boolean isMissingPermissions() {
-        for (String permission : REQUIRED_PERMISSIONS) {
-            if (ContextCompat.checkSelfPermission(this, permission)
-                    != PackageManager.PERMISSION_GRANTED) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     private void selectPages(@NonNull CharSequence title, @NonNull Pages pages) {
         ActionBar actionBar = getSupportActionBar();
         if (actionBar != null) {
@@ -210,32 +183,32 @@
     private static class ActivityPagerAdapter extends FragmentPagerAdapter {
         private Pages mPages;
 
-        private ActivityPagerAdapter(@NonNull FragmentManager fm) {
+        ActivityPagerAdapter(@NonNull FragmentManager fm) {
             super(fm);
         }
 
-        private void setPages(@NonNull Pages pages) {
+        void setPages(@NonNull Pages pages) {
             mPages = pages;
             notifyDataSetChanged();
         }
 
-        private @Nullable Pages getPages() {
+        @Nullable Pages getPages() {
             return mPages;
         }
 
         @Override
         public int getCount() {
-            return mPages.pages.length;
+            return mPages.getPages().length;
         }
 
         @Override
         public @NonNull Fragment getItem(int position) {
-            return mPages.pages[position].pageCreator.newInstance();
+            return mPages.getPages()[position].createFragment();
         }
 
         @Override
         public long getItemId(int position) {
-            return mPages.pages[position].id;
+            return mPages.getPages()[position].getId();
         }
 
         @Override
@@ -245,40 +218,87 @@
 
         @Override
         public @NonNull CharSequence getPageTitle(int position) {
-            return mPages.pages[position].title;
+            return mPages.getPages()[position].getTitle();
         }
     }
 
     private static class Page {
-        private static int sid;
-        private Page(@NonNull PageCreator pageCreator, @NonNull String title) {
-            this.id = sid++;
-            this.pageCreator = pageCreator;
-            this.title = title;
+        private static int sId;
+
+        private final int mId;
+        private final PageCreator mPageCreator;
+        private final String mTitle;
+
+        Page(@NonNull PageCreator pageCreator, @NonNull String title) {
+            mId = sId++;
+            mPageCreator = pageCreator;
+            mTitle = title;
         }
 
-        private final int id;
-        private final PageCreator pageCreator;
-        private final String title;
+        int getId() {
+            return mId;
+        }
+
+        @NonNull Fragment createFragment() {
+            return mPageCreator.newInstance();
+        }
+
+        @NonNull String getTitle() {
+            return mTitle;
+        }
     }
 
     private static class Pages {
-        private Pages(@IdRes int id, @NonNull Page[] pages) {
-            this.id = id;
-            this.pages = pages;
+        private final int mId;
+        private final Page[] mPages;
+
+        private int mCurrent;
+
+        Pages(@IdRes int id, @NonNull Page[] pages) {
+            mId = id;
+            mPages = pages;
         }
 
-        private final int id;
-        private final Page[] pages;
-
-        private int current;
-
-        private void setCurrent(int current) {
-            this.current = current;
+        int getId() {
+            return mId;
         }
 
-        private int getCurrent() {
-            return current;
+        @NonNull Page[] getPages() {
+            return mPages;
+        }
+
+        void setCurrent(int current) {
+            mCurrent = current;
+        }
+
+        int getCurrent() {
+            return mCurrent;
+        }
+    }
+
+    private static class PermissionPage extends Page {
+        PermissionPage(@NonNull PageCreator pageCreator, @NonNull String title) {
+            super(pageCreator, title);
+        }
+
+        @Override
+        int getId() {
+            if (isMissingPermissions()) {
+                return ~super.getId();
+            }
+            return super.getId();
+        }
+
+        @Override
+        @NonNull Fragment createFragment() {
+            if (isMissingPermissions()) {
+                return PermissionFragment.newInstance();
+            }
+            return super.createFragment();
+        }
+
+        private boolean isMissingPermissions() {
+            return sIsMissingPermissions;
         }
     }
 
diff --git a/java/com/android/pump/db/Artist.java b/java/com/android/pump/db/Artist.java
index eac3c4d..0270bda 100644
--- a/java/com/android/pump/db/Artist.java
+++ b/java/com/android/pump/db/Artist.java
@@ -16,6 +16,8 @@
 
 package com.android.pump.db;
 
+import android.net.Uri;
+
 import androidx.annotation.AnyThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -30,6 +32,8 @@
 
     // TODO(b/123706949) Lock mutable fields to ensure consistent updates
     private String mName;
+    private String mDescription;
+    private Uri mHeadshotUri;
     private final List<Album> mAlbums = new ArrayList<>();
     private final List<Audio> mAudios = new ArrayList<>();
     private boolean mLoaded;
@@ -54,6 +58,30 @@
         return Collections.unmodifiableList(mAudios);
     }
 
+    public @Nullable Uri getHeadshotUri() {
+        return mHeadshotUri;
+    }
+
+    public boolean setHeadshotUri(@NonNull Uri headshotUri) {
+        if (headshotUri.equals(mHeadshotUri)) {
+            return false;
+        }
+        mHeadshotUri = headshotUri;
+        return true;
+    }
+
+    public @Nullable String getDescription() {
+        return mDescription;
+    }
+
+    public boolean setDescription(@NonNull String description) {
+        if (description.equals(mDescription)) {
+            return false;
+        }
+        mDescription = description;
+        return true;
+    }
+
     boolean setName(@NonNull String name) {
         if (name.equals(mName)) {
             return false;
diff --git a/java/com/android/pump/db/DataProvider.java b/java/com/android/pump/db/DataProvider.java
index ab54324..c4511a7 100644
--- a/java/com/android/pump/db/DataProvider.java
+++ b/java/com/android/pump/db/DataProvider.java
@@ -20,7 +20,9 @@
 
 import java.io.IOException;
 
+// TODO (b/126977959): Split DataProvider into Audio/VideoDataProvider interfaces.
 public interface DataProvider {
+    boolean populateArtist(@NonNull Artist artist) throws IOException;
     boolean populateMovie(@NonNull Movie movie) throws IOException;
     boolean populateSeries(@NonNull Series series) throws IOException;
     boolean populateEpisode(@NonNull Episode episode) throws IOException;
diff --git a/java/com/android/pump/db/MediaDb.java b/java/com/android/pump/db/MediaDb.java
index 12de5e8..3c45531 100644
--- a/java/com/android/pump/db/MediaDb.java
+++ b/java/com/android/pump/db/MediaDb.java
@@ -263,12 +263,19 @@
         if (artist.isLoaded()) return;
 
         mExecutor.execute(() -> {
-            boolean updated = mAudioStore.loadData(artist);
+            try {
+                boolean updated = mDataProvider.populateArtist(artist);
 
-            artist.setLoaded();
-            if (updated) {
-                Executors.uiThreadExecutor().execute(() -> updateArtist(artist));
+                updated |= mAudioStore.loadData(artist);
+
+                artist.setLoaded();
+                if (updated) {
+                    Executors.uiThreadExecutor().execute(() -> updateArtist(artist));
+                }
+            } catch (IOException e) {
+                Clog.e(TAG, "Search for " + artist + " failed", e);
             }
+
         });
     }
 
diff --git a/java/com/android/pump/fragment/ArtistFragment.java b/java/com/android/pump/fragment/ArtistFragment.java
index caa92f7..41080f0 100644
--- a/java/com/android/pump/fragment/ArtistFragment.java
+++ b/java/com/android/pump/fragment/ArtistFragment.java
@@ -153,16 +153,20 @@
             ImageView imageView = itemView.findViewById(R.id.artist_image);
             TextView nameView = itemView.findViewById(R.id.artist_name);
 
-            // TODO This should be artist head shot rather than album art
-            Uri albumArtUri = null;
-            List<Album> albums = artist.getAlbums();
-            for (Album album : albums) {
-                if (album.getAlbumArtUri() != null) {
-                    albumArtUri = album.getAlbumArtUri();
-                    break;
+            Uri artUri = null;
+            if (artist.getHeadshotUri() == null) {
+                // Fallback to album art
+                List<Album> albums = artist.getAlbums();
+                for (Album album : albums) {
+                    if (album.getAlbumArtUri() != null) {
+                        artUri = album.getAlbumArtUri();
+                        break;
+                    }
                 }
+            } else {
+                artUri = artist.getHeadshotUri();
             }
-            imageView.setImageURI(albumArtUri);
+            imageView.setImageURI(artUri);
             nameView.setText(artist.getName());
 
             itemView.setOnClickListener((view) ->
diff --git a/java/com/android/pump/fragment/PermissionFragment.java b/java/com/android/pump/fragment/PermissionFragment.java
new file mode 100644
index 0000000..0e4aa68
--- /dev/null
+++ b/java/com/android/pump/fragment/PermissionFragment.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2019 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.pump.fragment;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.fragment.app.Fragment;
+
+import com.android.pump.R;
+import com.android.pump.activity.PumpActivity;
+
+@UiThread
+public class PermissionFragment extends Fragment {
+    public static @NonNull Fragment newInstance() {
+        return new PermissionFragment();
+    }
+
+    @Override
+    public @NonNull View onCreateView(@NonNull LayoutInflater inflater,
+            @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.fragment_permission, container, false);
+
+        view.findViewById(R.id.fragment_permission_button)
+                .setOnClickListener((v) -> PumpActivity.requestPermissions(getActivity()));
+
+        return view;
+    }
+}
diff --git a/java/com/android/pump/provider/KnowledgeGraph.java b/java/com/android/pump/provider/KnowledgeGraph.java
index d09293e..049e4d7 100644
--- a/java/com/android/pump/provider/KnowledgeGraph.java
+++ b/java/com/android/pump/provider/KnowledgeGraph.java
@@ -17,12 +17,15 @@
 package com.android.pump.provider;
 
 import android.net.Uri;
+import android.util.Log;
 import android.util.Pair;
 
 import androidx.annotation.AnyThread;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 
+import com.android.pump.db.Artist;
 import com.android.pump.db.DataProvider;
 import com.android.pump.db.Episode;
 import com.android.pump.db.Movie;
@@ -30,14 +33,14 @@
 import com.android.pump.util.Clog;
 import com.android.pump.util.Http;
 
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.json.JSONTokener;
 
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-
 @WorkerThread
 public final class KnowledgeGraph implements DataProvider {
     private static final String TAG = Clog.tag(KnowledgeGraph.class);
@@ -52,14 +55,38 @@
     }
 
     @Override
+    public boolean populateArtist(@NonNull Artist artist) throws IOException {
+        boolean updated = false;
+        // Artist may be of type "Person" or "MusicGroup"
+        JSONObject result = getResultFromKG(artist.getName(), "Person");
+        if (result == null) {
+            result = getResultFromKG(artist.getName(), "MusicGroup");
+        }
+
+        Pair<String, String> metadata = getMetadataFromResult(result);
+        if (metadata != null) {
+            if (metadata.first != null) {
+                updated |= artist.setHeadshotUri(Uri.parse(metadata.first));
+            }
+            if (metadata.second != null) {
+                updated |= artist.setDescription(metadata.second);
+            }
+        }
+        return updated;
+    }
+
+    @Override
     public boolean populateMovie(@NonNull Movie movie) throws IOException {
         boolean updated = false;
-        Pair<String, String> metadata = getMetadata(movie.getTitle(), "Movie");
-        if (metadata.first != null) {
-            updated |= movie.setPosterUri(Uri.parse(metadata.first));
-        }
-        if (metadata.second != null) {
-            updated |= movie.setDescription(metadata.second);
+        Pair<String, String> metadata = getMetadataFromResult(
+                getResultFromKG(movie.getTitle(), "Movie"));
+        if (metadata != null) {
+            if (metadata.first != null) {
+                updated |= movie.setPosterUri(Uri.parse(metadata.first));
+            }
+            if (metadata.second != null) {
+                updated |= movie.setDescription(metadata.second);
+            }
         }
         return updated;
     }
@@ -67,12 +94,15 @@
     @Override
     public boolean populateSeries(@NonNull Series series) throws IOException {
         boolean updated = false;
-        Pair<String, String> metadata = getMetadata(series.getTitle(), "TVSeries");
-        if (metadata.first != null) {
-            updated |= series.setPosterUri(Uri.parse(metadata.first));
-        }
-        if (metadata.second != null) {
-            updated |= series.setDescription(metadata.second);
+        Pair<String, String> metadata = getMetadataFromResult(
+                getResultFromKG(series.getTitle(), "TVSeries"));
+        if (metadata != null) {
+            if (metadata.first != null) {
+                updated |= series.setPosterUri(Uri.parse(metadata.first));
+            }
+            if (metadata.second != null) {
+                updated |= series.setDescription(metadata.second);
+            }
         }
         return updated;
     }
@@ -80,24 +110,44 @@
     @Override
     public boolean populateEpisode(@NonNull Episode episode) throws IOException {
         boolean updated = false;
-        Pair<String, String> metadata = getMetadata(episode.getTitle(), "TVEpisode");
-        if (metadata.first != null) {
-            updated |= episode.setPosterUri(Uri.parse(metadata.first));
-        }
-        if (metadata.second != null) {
-            updated |= episode.setDescription(metadata.second);
+        Pair<String, String> metadata = getMetadataFromResult(
+                getResultFromKG(episode.getTitle(), "TVEpisode"));
+        if (metadata != null) {
+            if (metadata.first != null) {
+                updated |= episode.setPosterUri(Uri.parse(metadata.first));
+            }
+            if (metadata.second != null) {
+                updated |= episode.setDescription(metadata.second);
+            }
         }
         return updated;
     }
 
-    private Pair<String, String> getMetadata(String title, String type) throws IOException {
-        String imageUrl = null;
-        String description = null;
+    private JSONObject getResultFromKG(String title, String type) throws IOException {
         try {
             JSONObject root = (JSONObject) getContent(getContentUri(title, type));
             JSONArray items = root.getJSONArray("itemListElement");
             JSONObject item = (JSONObject) items.get(0);
             JSONObject result = item.getJSONObject("result");
+            if (!title.equals(result.getString("name"))) {
+                return null;
+            }
+            return result;
+        } catch (JSONException e) {
+            Clog.w(TAG, "Failed to find search result", e);
+            return null;
+        }
+    }
+
+    private Pair<String, String> getMetadataFromResult(@NonNull JSONObject result)
+            throws IOException {
+        if (result == null) {
+            throw new IOException("Failed to find search result");
+        }
+
+        String imageUrl = null;
+        String description = null;
+        try {
             JSONObject image = result.optJSONObject("image");
             if (image != null) {
                 String url = image.getString("contentUrl");
@@ -117,7 +167,7 @@
         return new Pair<>(imageUrl, description);
     }
 
-    private static @NonNull Uri getContentUri(@NonNull String title, @NonNull String type) {
+    private static @NonNull Uri getContentUri(@NonNull String title, @Nullable String type) {
         Uri.Builder ub = new Uri.Builder();
         ub.scheme("https");
         ub.authority("kgsearch.googleapis.com");
diff --git a/java/com/android/pump/provider/OmdbApi.java b/java/com/android/pump/provider/OmdbApi.java
index 5a7876c..ac7a6cd 100644
--- a/java/com/android/pump/provider/OmdbApi.java
+++ b/java/com/android/pump/provider/OmdbApi.java
@@ -22,6 +22,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.WorkerThread;
 
+import com.android.pump.db.Artist;
 import com.android.pump.db.DataProvider;
 import com.android.pump.db.Episode;
 import com.android.pump.db.Movie;
@@ -50,6 +51,12 @@
     }
 
     @Override
+    public boolean populateArtist(@NonNull Artist artist) throws IOException {
+        // NO-OP
+        return false;
+    }
+
+    @Override
     public boolean populateMovie(@NonNull Movie movie) throws IOException {
         boolean updated = false;
         try {
diff --git a/java/com/android/pump/util/Permissions.java b/java/com/android/pump/util/Permissions.java
new file mode 100644
index 0000000..9f1cd94
--- /dev/null
+++ b/java/com/android/pump/util/Permissions.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2019 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.pump.util;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.UiThread;
+import androidx.core.app.ActivityCompat;
+
+@UiThread
+public final class Permissions {
+    private static final String[] REQUIRED_PERMISSIONS = {
+            android.Manifest.permission.INTERNET,
+            android.Manifest.permission.READ_EXTERNAL_STORAGE,
+            android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+    };
+
+    private Permissions() { }
+
+    public static boolean isMissingPermissions(@NonNull Context context) {
+        for (String permission : REQUIRED_PERMISSIONS) {
+            if (ActivityCompat.checkSelfPermission(context, permission)
+                    != PackageManager.PERMISSION_GRANTED) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static boolean requestMissingPermissions(@NonNull Activity activity,
+            @IntRange(from = 0) int requestCode) {
+        if (isMissingPermissions(activity)) {
+            ActivityCompat.requestPermissions(activity, REQUIRED_PERMISSIONS, requestCode);
+            return true;
+        }
+        return false;
+    }
+
+    public static boolean isGranted(@NonNull String[] permissions, @NonNull int[] grantResults) {
+        if (grantResults.length == 0) {
+            return false;
+        } else {
+            for (int grantResult : grantResults) {
+                if (grantResult != PackageManager.PERMISSION_GRANTED) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+}
diff --git a/res/layout/fragment_permission.xml b/res/layout/fragment_permission.xml
new file mode 100644
index 0000000..8a0908c
--- /dev/null
+++ b/res/layout/fragment_permission.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 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.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <com.android.pump.widget.UriImageView
+        android:id="@+id/fragment_permission_image"
+        android:layout_width="222dp"
+        android:layout_height="222dp"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/fragment_permission_text1"
+        app:layout_constraintVertical_chainStyle="packed"
+        app:srcCompat="@drawable/ic_placeholder"/>
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/fragment_permission_text1"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="24dp"
+        android:layout_marginStart="24dp"
+        android:layout_marginEnd="24dp"
+        android:gravity="center_horizontal"
+        android:textSize="24sp"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/fragment_permission_image"
+        app:layout_constraintBottom_toTopOf="@id/fragment_permission_text2"
+        android:text="Start the show"/>
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/fragment_permission_text2"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="24dp"
+        android:layout_marginStart="24dp"
+        android:layout_marginEnd="24dp"
+        android:gravity="center_horizontal"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/fragment_permission_text1"
+        app:layout_constraintBottom_toTopOf="@id/fragment_permission_button"
+        android:text=
+            "To play your videos and audios, allow access to the media files on your device."/>
+
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/fragment_permission_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="24dp"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/fragment_permission_text2"
+        app:layout_constraintBottom_toBottomOf="parent"
+        android:text="Allow Access"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>